+3
-1
.gitignore
+3
-1
.gitignore
+1
-1
Cargo.toml
+1
-1
Cargo.toml
+9
-3
RULES.md
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
-2
src/cli/cmd_server.rs
+48
-3
src/cli/cmd_stats.rs
+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
+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
+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
+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
+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
+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
-
¤t_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
+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
+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
-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
-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
-