1use anyhow::{Context, Result, bail};
4use cargo_generate::{GenerateArgs, TemplatePath, Vcs};
5use clap::{Parser, Subcommand};
6use flate2::read::GzDecoder;
7use serde::Deserialize;
8use std::collections::BTreeMap;
9use std::io::IsTerminal;
10use std::path::Path;
11use tar::Archive;
12
13mod tui;
14
15const CRATES_IO_API: &str = "https://crates.io/api/v1/crates";
16const CRATES_IO_CDN: &str = "https://static.crates.io/crates";
17
18#[derive(Parser)]
19#[command(name = "cargo-bp")]
20#[command(bin_name = "cargo")]
21#[command(version, about = "Create and manage battery packs", long_about = None)]
22pub struct Cli {
23 #[command(subcommand)]
24 pub command: Commands,
25}
26
27#[derive(Subcommand)]
28pub enum Commands {
29 Bp {
31 #[command(subcommand)]
32 command: BpCommands,
33 },
34}
35
36#[derive(Subcommand)]
37pub enum BpCommands {
38 New {
40 battery_pack: String,
42
43 #[arg(long, short = 'n')]
45 name: Option<String>,
46
47 #[arg(long, short = 't')]
49 template: Option<String>,
50
51 #[arg(long)]
53 path: Option<String>,
54 },
55
56 Add {
58 battery_pack: String,
60
61 #[arg(long, short = 'w')]
63 with: Vec<String>,
64
65 #[arg(long)]
67 all: bool,
68
69 #[arg(long)]
71 path: Option<String>,
72 },
73
74 Sync,
76
77 Enable {
79 set_name: String,
81
82 #[arg(long)]
84 battery_pack: Option<String>,
85 },
86
87 List {
89 filter: Option<String>,
91
92 #[arg(long)]
94 non_interactive: bool,
95 },
96
97 Show {
99 battery_pack: String,
101
102 #[arg(long)]
104 path: Option<String>,
105
106 #[arg(long)]
108 non_interactive: bool,
109 },
110}
111
112pub fn main() -> Result<()> {
114 let cli = Cli::parse();
115
116 match cli.command {
117 Commands::Bp { command } => match command {
118 BpCommands::New {
119 battery_pack,
120 name,
121 template,
122 path,
123 } => new_from_battery_pack(&battery_pack, name, template, path),
124 BpCommands::Add {
125 battery_pack,
126 with,
127 all,
128 path,
129 } => add_battery_pack(&battery_pack, &with, all, path.as_deref()),
130 BpCommands::Sync => sync_battery_packs(),
131 BpCommands::Enable {
132 set_name,
133 battery_pack,
134 } => enable_set(&set_name, battery_pack.as_deref()),
135 BpCommands::List {
136 filter,
137 non_interactive,
138 } => {
139 if !non_interactive && std::io::stdout().is_terminal() {
140 tui::run_list(filter)
141 } else {
142 print_battery_pack_list(filter.as_deref())
143 }
144 }
145 BpCommands::Show {
146 battery_pack,
147 path,
148 non_interactive,
149 } => {
150 if !non_interactive && std::io::stdout().is_terminal() {
151 tui::run_show(&battery_pack, path.as_deref())
152 } else {
153 print_battery_pack_detail(&battery_pack, path.as_deref())
154 }
155 }
156 },
157 }
158}
159
160#[derive(Deserialize)]
165struct CratesIoResponse {
166 versions: Vec<VersionInfo>,
167}
168
169#[derive(Deserialize)]
170struct VersionInfo {
171 num: String,
172 yanked: bool,
173}
174
175#[derive(Deserialize)]
176struct SearchResponse {
177 crates: Vec<SearchCrate>,
178}
179
180#[derive(Deserialize)]
181struct SearchCrate {
182 name: String,
183 max_version: String,
184 description: Option<String>,
185}
186
187#[derive(Deserialize, Default)]
192struct CargoManifest {
193 package: Option<PackageSection>,
194 #[serde(default)]
195 dependencies: BTreeMap<String, toml::Value>,
196}
197
198#[derive(Deserialize, Default)]
199struct PackageSection {
200 name: Option<String>,
201 version: Option<String>,
202 description: Option<String>,
203 repository: Option<String>,
204 metadata: Option<PackageMetadata>,
205}
206
207#[derive(Deserialize, Default)]
208struct PackageMetadata {
209 battery: Option<BatteryMetadata>,
210}
211
212#[derive(Deserialize, Default)]
213struct BatteryMetadata {
214 #[serde(default)]
215 templates: BTreeMap<String, TemplateConfig>,
216}
217
218#[derive(Deserialize)]
219struct TemplateConfig {
220 path: String,
221 #[serde(default)]
222 description: Option<String>,
223}
224
225#[derive(Deserialize)]
230struct OwnersResponse {
231 users: Vec<Owner>,
232}
233
234#[derive(Deserialize, Clone)]
235struct Owner {
236 login: String,
237 name: Option<String>,
238}
239
240#[derive(Deserialize)]
245struct GitHubTreeResponse {
246 tree: Vec<GitHubTreeEntry>,
247 #[serde(default)]
248 #[allow(dead_code)]
249 truncated: bool,
250}
251
252#[derive(Deserialize)]
253struct GitHubTreeEntry {
254 path: String,
255}
256
257#[derive(Clone)]
263pub struct BatteryPackSummary {
264 pub name: String,
265 pub short_name: String,
266 pub version: String,
267 pub description: String,
268}
269
270#[derive(Clone)]
272pub struct BatteryPackDetail {
273 pub name: String,
274 pub short_name: String,
275 pub version: String,
276 pub description: String,
277 pub repository: Option<String>,
278 pub owners: Vec<OwnerInfo>,
279 pub crates: Vec<String>,
280 pub extends: Vec<String>,
281 pub templates: Vec<TemplateInfo>,
282 pub examples: Vec<ExampleInfo>,
283}
284
285#[derive(Clone)]
286pub struct OwnerInfo {
287 pub login: String,
288 pub name: Option<String>,
289}
290
291impl From<Owner> for OwnerInfo {
292 fn from(o: Owner) -> Self {
293 Self {
294 login: o.login,
295 name: o.name,
296 }
297 }
298}
299
300#[derive(Clone)]
301pub struct TemplateInfo {
302 pub name: String,
303 pub path: String,
304 pub description: Option<String>,
305 pub repo_path: Option<String>,
308}
309
310#[derive(Clone)]
311pub struct ExampleInfo {
312 pub name: String,
313 pub description: Option<String>,
314 pub repo_path: Option<String>,
317}
318
319fn new_from_battery_pack(
324 battery_pack: &str,
325 name: Option<String>,
326 template: Option<String>,
327 path_override: Option<String>,
328) -> Result<()> {
329 if let Some(path) = path_override {
331 return generate_from_local(&path, name, template);
332 }
333
334 let crate_name = resolve_crate_name(battery_pack);
336
337 let crate_info = lookup_crate(&crate_name)?;
339
340 let temp_dir = download_and_extract_crate(&crate_name, &crate_info.version)?;
342 let crate_dir = temp_dir
343 .path()
344 .join(format!("{}-{}", crate_name, crate_info.version));
345
346 let manifest_path = crate_dir.join("Cargo.toml");
348 let manifest_content = std::fs::read_to_string(&manifest_path)
349 .with_context(|| format!("Failed to read {}", manifest_path.display()))?;
350 let templates = parse_template_metadata(&manifest_content, &crate_name)?;
351
352 let template_path = resolve_template(&templates, template.as_deref())?;
354
355 generate_from_path(&crate_dir, &template_path, name)
357}
358
359fn add_battery_pack(name: &str, with_sets: &[String], all: bool, path: Option<&str>) -> Result<()> {
360 let crate_name = resolve_crate_name(name);
361
362 let bp_version = if path.is_some() {
364 None
365 } else {
366 Some(lookup_crate(&crate_name)?.version)
367 };
368
369 let user_manifest_path = find_user_manifest()?;
371 let user_manifest_content =
372 std::fs::read_to_string(&user_manifest_path).context("Failed to read Cargo.toml")?;
373 let mut user_doc: toml_edit::DocumentMut = user_manifest_content
374 .parse()
375 .context("Failed to parse Cargo.toml")?;
376
377 let workspace_manifest = find_workspace_manifest(&user_manifest_path)?;
378
379 let build_deps =
380 user_doc["build-dependencies"].or_insert(toml_edit::Item::Table(toml_edit::Table::new()));
381 if let Some(table) = build_deps.as_table_mut() {
382 if let Some(local_path) = path {
383 let mut dep = toml_edit::InlineTable::new();
384 dep.insert("path", toml_edit::Value::from(local_path));
385 table.insert(
386 &crate_name,
387 toml_edit::Item::Value(toml_edit::Value::InlineTable(dep)),
388 );
389 } else if workspace_manifest.is_some() {
390 let mut dep = toml_edit::InlineTable::new();
391 dep.insert("workspace", toml_edit::Value::from(true));
392 table.insert(
393 &crate_name,
394 toml_edit::Item::Value(toml_edit::Value::InlineTable(dep)),
395 );
396 } else {
397 table.insert(&crate_name, toml_edit::value(bp_version.as_ref().unwrap()));
398 }
399 }
400
401 if let Some(ref ws_path) = workspace_manifest {
403 let ws_content =
404 std::fs::read_to_string(ws_path).context("Failed to read workspace Cargo.toml")?;
405 let mut ws_doc: toml_edit::DocumentMut = ws_content
406 .parse()
407 .context("Failed to parse workspace Cargo.toml")?;
408
409 let ws_deps = ws_doc["workspace"]["dependencies"]
410 .or_insert(toml_edit::Item::Table(toml_edit::Table::new()));
411 if let Some(ws_table) = ws_deps.as_table_mut() {
412 if let Some(local_path) = path {
413 let mut dep = toml_edit::InlineTable::new();
414 dep.insert("path", toml_edit::Value::from(local_path));
415 ws_table.insert(
416 &crate_name,
417 toml_edit::Item::Value(toml_edit::Value::InlineTable(dep)),
418 );
419 } else {
420 ws_table.insert(&crate_name, toml_edit::value(bp_version.as_ref().unwrap()));
421 }
422 }
423 std::fs::write(ws_path, ws_doc.to_string())
424 .context("Failed to write workspace Cargo.toml")?;
425 }
426
427 std::fs::write(&user_manifest_path, user_doc.to_string())
429 .context("Failed to write Cargo.toml")?;
430
431 let bp_spec = fetch_battery_pack_spec(&crate_name)?;
433
434 let active_sets = if all {
436 vec!["default".to_string()]
437 } else {
438 let mut sets = vec!["default".to_string()];
439 sets.extend(with_sets.iter().cloned());
440 sets
441 };
442
443 let crates_to_sync = if all {
444 bp_spec.resolve_all()
445 } else {
446 bp_spec.resolve_crates(&active_sets)
447 };
448
449 let user_manifest_content =
451 std::fs::read_to_string(&user_manifest_path).context("Failed to read Cargo.toml")?;
452 let mut user_doc: toml_edit::DocumentMut = user_manifest_content
453 .parse()
454 .context("Failed to parse Cargo.toml")?;
455
456 if let Some(ref ws_path) = workspace_manifest {
457 let ws_content =
458 std::fs::read_to_string(ws_path).context("Failed to read workspace Cargo.toml")?;
459 let mut ws_doc: toml_edit::DocumentMut = ws_content
460 .parse()
461 .context("Failed to parse workspace Cargo.toml")?;
462
463 let ws_deps = ws_doc["workspace"]["dependencies"]
464 .or_insert(toml_edit::Item::Table(toml_edit::Table::new()));
465 if let Some(ws_table) = ws_deps.as_table_mut() {
466 for (dep_name, dep_spec) in &crates_to_sync {
467 add_dep_to_table(ws_table, dep_name, dep_spec);
468 }
469 }
470 std::fs::write(ws_path, ws_doc.to_string())
471 .context("Failed to write workspace Cargo.toml")?;
472
473 let deps =
474 user_doc["dependencies"].or_insert(toml_edit::Item::Table(toml_edit::Table::new()));
475 if let Some(table) = deps.as_table_mut() {
476 for dep_name in crates_to_sync.keys() {
477 let mut dep = toml_edit::InlineTable::new();
478 dep.insert("workspace", toml_edit::Value::from(true));
479 table.insert(
480 dep_name,
481 toml_edit::Item::Value(toml_edit::Value::InlineTable(dep)),
482 );
483 }
484 }
485 } else {
486 let deps =
487 user_doc["dependencies"].or_insert(toml_edit::Item::Table(toml_edit::Table::new()));
488 if let Some(table) = deps.as_table_mut() {
489 for (dep_name, dep_spec) in &crates_to_sync {
490 add_dep_to_table(table, dep_name, dep_spec);
491 }
492 }
493 }
494
495 let bp_meta = &mut user_doc["package"]["metadata"]["battery-pack"][&crate_name];
497 let mut sets_array = toml_edit::Array::new();
498 if all {
499 sets_array.push("all");
500 } else {
501 for set in &active_sets {
502 sets_array.push(set.as_str());
503 }
504 }
505 *bp_meta = toml_edit::Item::Table(toml_edit::Table::new());
506 bp_meta["sets"] = toml_edit::value(sets_array);
507
508 std::fs::write(&user_manifest_path, user_doc.to_string())
510 .context("Failed to write Cargo.toml")?;
511
512 let build_rs_path = user_manifest_path
514 .parent()
515 .unwrap_or(Path::new("."))
516 .join("build.rs");
517 update_build_rs(&build_rs_path, &crate_name)?;
518
519 println!(
520 "Added {} with {} crate(s)",
521 crate_name,
522 crates_to_sync.len()
523 );
524 for dep_name in crates_to_sync.keys() {
525 println!(" + {}", dep_name);
526 }
527
528 Ok(())
529}
530
531fn sync_battery_packs() -> Result<()> {
532 let user_manifest_path = find_user_manifest()?;
533 let user_manifest_content =
534 std::fs::read_to_string(&user_manifest_path).context("Failed to read Cargo.toml")?;
535
536 let raw: toml::Value =
538 toml::from_str(&user_manifest_content).context("Failed to parse Cargo.toml")?;
539
540 let build_deps = raw
541 .get("build-dependencies")
542 .and_then(|bd| bd.as_table())
543 .cloned()
544 .unwrap_or_default();
545
546 let bp_names: Vec<String> = build_deps
547 .keys()
548 .filter(|k| k.ends_with("-battery-pack") || *k == "battery-pack")
549 .cloned()
550 .collect();
551
552 if bp_names.is_empty() {
553 println!("No battery packs installed.");
554 return Ok(());
555 }
556
557 let mut user_doc: toml_edit::DocumentMut = user_manifest_content
558 .parse()
559 .context("Failed to parse Cargo.toml")?;
560
561 let workspace_manifest = find_workspace_manifest(&user_manifest_path)?;
562 let mut total_changes = 0;
563
564 for bp_name in &bp_names {
565 let bp_spec = fetch_battery_pack_spec(bp_name)?;
567
568 let active_sets = read_active_sets(&user_manifest_content, bp_name);
570
571 let expected = if active_sets.iter().any(|s| s == "all") {
572 bp_spec.resolve_all()
573 } else {
574 bp_spec.resolve_crates(&active_sets)
575 };
576
577 if let Some(ref ws_path) = workspace_manifest {
579 let ws_content =
580 std::fs::read_to_string(ws_path).context("Failed to read workspace Cargo.toml")?;
581 let mut ws_doc: toml_edit::DocumentMut = ws_content
582 .parse()
583 .context("Failed to parse workspace Cargo.toml")?;
584
585 let ws_deps = ws_doc["workspace"]["dependencies"]
586 .or_insert(toml_edit::Item::Table(toml_edit::Table::new()));
587 if let Some(ws_table) = ws_deps.as_table_mut() {
588 for (dep_name, dep_spec) in &expected {
589 if sync_dep_in_table(ws_table, dep_name, dep_spec) {
590 total_changes += 1;
591 println!(" ~ {} (updated in workspace)", dep_name);
592 }
593 }
594 }
595 std::fs::write(ws_path, ws_doc.to_string())
596 .context("Failed to write workspace Cargo.toml")?;
597
598 let deps =
600 user_doc["dependencies"].or_insert(toml_edit::Item::Table(toml_edit::Table::new()));
601 if let Some(table) = deps.as_table_mut() {
602 for dep_name in expected.keys() {
603 if !table.contains_key(dep_name) {
604 let mut dep = toml_edit::InlineTable::new();
605 dep.insert("workspace", toml_edit::Value::from(true));
606 table.insert(
607 dep_name,
608 toml_edit::Item::Value(toml_edit::Value::InlineTable(dep)),
609 );
610 total_changes += 1;
611 println!(" + {} (added workspace reference)", dep_name);
612 }
613 }
614 }
615 } else {
616 let deps =
617 user_doc["dependencies"].or_insert(toml_edit::Item::Table(toml_edit::Table::new()));
618 if let Some(table) = deps.as_table_mut() {
619 for (dep_name, dep_spec) in &expected {
620 if !table.contains_key(dep_name) {
621 add_dep_to_table(table, dep_name, dep_spec);
622 total_changes += 1;
623 println!(" + {}", dep_name);
624 } else if sync_dep_in_table(table, dep_name, dep_spec) {
625 total_changes += 1;
626 println!(" ~ {}", dep_name);
627 }
628 }
629 }
630 }
631 }
632
633 std::fs::write(&user_manifest_path, user_doc.to_string())
634 .context("Failed to write Cargo.toml")?;
635
636 if total_changes == 0 {
637 println!("All dependencies are up to date.");
638 } else {
639 println!("Synced {} change(s).", total_changes);
640 }
641
642 Ok(())
643}
644
645fn enable_set(set_name: &str, battery_pack: Option<&str>) -> Result<()> {
646 let user_manifest_path = find_user_manifest()?;
647 let user_manifest_content =
648 std::fs::read_to_string(&user_manifest_path).context("Failed to read Cargo.toml")?;
649
650 let raw: toml::Value =
651 toml::from_str(&user_manifest_content).context("Failed to parse Cargo.toml")?;
652
653 let build_deps = raw
654 .get("build-dependencies")
655 .and_then(|bd| bd.as_table())
656 .cloned()
657 .unwrap_or_default();
658
659 let bp_name = if let Some(name) = battery_pack {
661 resolve_crate_name(name)
662 } else {
663 let bp_names: Vec<String> = build_deps
665 .keys()
666 .filter(|k| k.ends_with("-battery-pack") || *k == "battery-pack")
667 .cloned()
668 .collect();
669
670 let mut found = None;
671 for name in &bp_names {
672 let spec = fetch_battery_pack_spec(name)?;
673 if spec.sets.contains_key(set_name) {
674 found = Some(name.clone());
675 break;
676 }
677 }
678 found.ok_or_else(|| {
679 anyhow::anyhow!("No installed battery pack defines set '{}'", set_name)
680 })?
681 };
682
683 let bp_spec = fetch_battery_pack_spec(&bp_name)?;
684
685 if !bp_spec.sets.contains_key(set_name) {
686 let available: Vec<_> = bp_spec.sets.keys().collect();
687 bail!(
688 "Battery pack '{}' has no set '{}'. Available: {:?}",
689 bp_name,
690 set_name,
691 available
692 );
693 }
694
695 let mut active_sets = read_active_sets(&user_manifest_content, &bp_name);
697 if active_sets.contains(&set_name.to_string()) {
698 println!("Set '{}' is already active for {}.", set_name, bp_name);
699 return Ok(());
700 }
701 active_sets.push(set_name.to_string());
702
703 let crates_to_sync = bp_spec.resolve_crates(&active_sets);
705
706 let mut user_doc: toml_edit::DocumentMut = user_manifest_content
708 .parse()
709 .context("Failed to parse Cargo.toml")?;
710
711 let workspace_manifest = find_workspace_manifest(&user_manifest_path)?;
712
713 if let Some(ref ws_path) = workspace_manifest {
715 let ws_content =
716 std::fs::read_to_string(ws_path).context("Failed to read workspace Cargo.toml")?;
717 let mut ws_doc: toml_edit::DocumentMut = ws_content
718 .parse()
719 .context("Failed to parse workspace Cargo.toml")?;
720
721 let ws_deps = ws_doc["workspace"]["dependencies"]
722 .or_insert(toml_edit::Item::Table(toml_edit::Table::new()));
723 if let Some(ws_table) = ws_deps.as_table_mut() {
724 for (dep_name, dep_spec) in &crates_to_sync {
725 add_dep_to_table(ws_table, dep_name, dep_spec);
726 }
727 }
728 std::fs::write(ws_path, ws_doc.to_string())
729 .context("Failed to write workspace Cargo.toml")?;
730
731 let deps =
732 user_doc["dependencies"].or_insert(toml_edit::Item::Table(toml_edit::Table::new()));
733 if let Some(table) = deps.as_table_mut() {
734 for dep_name in crates_to_sync.keys() {
735 if !table.contains_key(dep_name) {
736 let mut dep = toml_edit::InlineTable::new();
737 dep.insert("workspace", toml_edit::Value::from(true));
738 table.insert(
739 dep_name,
740 toml_edit::Item::Value(toml_edit::Value::InlineTable(dep)),
741 );
742 }
743 }
744 }
745 } else {
746 let deps =
747 user_doc["dependencies"].or_insert(toml_edit::Item::Table(toml_edit::Table::new()));
748 if let Some(table) = deps.as_table_mut() {
749 for (dep_name, dep_spec) in &crates_to_sync {
750 if !table.contains_key(dep_name) {
751 add_dep_to_table(table, dep_name, dep_spec);
752 }
753 }
754 }
755 }
756
757 let bp_meta = &mut user_doc["package"]["metadata"]["battery-pack"][&bp_name];
759 let mut sets_array = toml_edit::Array::new();
760 for set in &active_sets {
761 sets_array.push(set.as_str());
762 }
763 *bp_meta = toml_edit::Item::Table(toml_edit::Table::new());
764 bp_meta["sets"] = toml_edit::value(sets_array);
765
766 std::fs::write(&user_manifest_path, user_doc.to_string())
767 .context("Failed to write Cargo.toml")?;
768
769 println!("Enabled set '{}' from {}", set_name, bp_name);
770 Ok(())
771}
772
773fn find_user_manifest() -> Result<std::path::PathBuf> {
779 let path = std::path::PathBuf::from("Cargo.toml");
780 if path.exists() {
781 Ok(path)
782 } else {
783 bail!("No Cargo.toml found in the current directory");
784 }
785}
786
787fn find_workspace_manifest(crate_manifest: &Path) -> Result<Option<std::path::PathBuf>> {
790 let crate_dir = crate_manifest
791 .parent()
792 .unwrap_or(Path::new("."))
793 .canonicalize()
794 .context("Failed to resolve crate directory")?;
795
796 let mut dir = crate_dir.clone();
798 loop {
799 let candidate = dir.join("Cargo.toml");
800 if candidate.exists() && candidate != crate_dir.join("Cargo.toml") {
801 let content = std::fs::read_to_string(&candidate)?;
802 if content.contains("[workspace]") {
803 return Ok(Some(candidate));
804 }
805 }
806 if !dir.pop() {
807 break;
808 }
809 }
810
811 Ok(None)
814}
815
816fn add_dep_to_table(table: &mut toml_edit::Table, name: &str, spec: &bphelper_manifest::DepSpec) {
818 if spec.features.is_empty() {
819 table.insert(name, toml_edit::value(&spec.version));
820 } else {
821 let mut dep = toml_edit::InlineTable::new();
822 dep.insert("version", toml_edit::Value::from(spec.version.as_str()));
823 let mut features = toml_edit::Array::new();
824 for feat in &spec.features {
825 features.push(feat.as_str());
826 }
827 dep.insert("features", toml_edit::Value::Array(features));
828 table.insert(
829 name,
830 toml_edit::Item::Value(toml_edit::Value::InlineTable(dep)),
831 );
832 }
833}
834
835fn sync_dep_in_table(
838 table: &mut toml_edit::Table,
839 name: &str,
840 spec: &bphelper_manifest::DepSpec,
841) -> bool {
842 let Some(existing) = table.get_mut(name) else {
843 add_dep_to_table(table, name, spec);
845 return true;
846 };
847
848 let mut changed = false;
849
850 match existing {
851 toml_edit::Item::Value(toml_edit::Value::String(version_str)) => {
852 let current = version_str.value().to_string();
854 if !spec.version.is_empty() && current != spec.version {
855 *version_str = toml_edit::Formatted::new(spec.version.clone());
856 changed = true;
857 }
858 if !spec.features.is_empty() {
859 add_dep_to_table(table, name, spec);
861 changed = true;
862 }
863 }
864 toml_edit::Item::Value(toml_edit::Value::InlineTable(inline)) => {
865 if let Some(toml_edit::Value::String(v)) = inline.get_mut("version") {
867 if !spec.version.is_empty() && v.value() != &spec.version {
868 *v = toml_edit::Formatted::new(spec.version.clone());
869 changed = true;
870 }
871 }
872 if !spec.features.is_empty() {
874 let existing_features: Vec<String> = inline
875 .get("features")
876 .and_then(|f| f.as_array())
877 .map(|arr| {
878 arr.iter()
879 .filter_map(|v| v.as_str().map(String::from))
880 .collect()
881 })
882 .unwrap_or_default();
883
884 let mut needs_update = false;
885 let mut all_features = existing_features.clone();
886 for feat in &spec.features {
887 if !existing_features.contains(feat) {
888 all_features.push(feat.clone());
889 needs_update = true;
890 }
891 }
892
893 if needs_update {
894 let mut arr = toml_edit::Array::new();
895 for f in &all_features {
896 arr.push(f.as_str());
897 }
898 inline.insert("features", toml_edit::Value::Array(arr));
899 changed = true;
900 }
901 }
902 }
903 _ => {}
904 }
905
906 changed
907}
908
909fn read_active_sets(manifest_content: &str, bp_name: &str) -> Vec<String> {
911 let raw: toml::Value = match toml::from_str(manifest_content) {
912 Ok(v) => v,
913 Err(_) => return vec!["default".to_string()],
914 };
915
916 raw.get("package")
917 .and_then(|p| p.get("metadata"))
918 .and_then(|m| m.get("battery-pack"))
919 .and_then(|bp| bp.get(bp_name))
920 .and_then(|entry| entry.get("sets"))
921 .and_then(|sets| sets.as_array())
922 .map(|arr| {
923 arr.iter()
924 .filter_map(|v| v.as_str().map(String::from))
925 .collect()
926 })
927 .unwrap_or_else(|| vec!["default".to_string()])
928}
929
930fn resolve_battery_pack_manifest(bp_name: &str) -> Result<std::path::PathBuf> {
935 let metadata = cargo_metadata::MetadataCommand::new()
936 .exec()
937 .context("Failed to run `cargo metadata`")?;
938
939 let package = metadata
940 .packages
941 .iter()
942 .find(|p| p.name == bp_name)
943 .ok_or_else(|| {
944 anyhow::anyhow!(
945 "Battery pack '{}' not found in dependency graph. Is it in [build-dependencies]?",
946 bp_name
947 )
948 })?;
949
950 Ok(package.manifest_path.clone().into())
951}
952
953fn fetch_battery_pack_spec(bp_name: &str) -> Result<bphelper_manifest::BatteryPackSpec> {
955 let manifest_path = resolve_battery_pack_manifest(bp_name)?;
956 let manifest_content = std::fs::read_to_string(&manifest_path)
957 .with_context(|| format!("Failed to read {}", manifest_path.display()))?;
958
959 bphelper_manifest::parse_battery_pack(&manifest_content)
960 .map_err(|e| anyhow::anyhow!("Failed to parse battery pack '{}': {}", bp_name, e))
961}
962
963fn update_build_rs(build_rs_path: &Path, crate_name: &str) -> Result<()> {
969 let crate_ident = crate_name.replace('-', "_");
970 let validate_call = format!("{}::validate();", crate_ident);
971
972 if build_rs_path.exists() {
973 let content = std::fs::read_to_string(build_rs_path).context("Failed to read build.rs")?;
974
975 if content.contains(&validate_call) {
977 return Ok(());
978 }
979
980 let file: syn::File = syn::parse_str(&content).context("Failed to parse build.rs")?;
982
983 let has_main = file
985 .items
986 .iter()
987 .any(|item| matches!(item, syn::Item::Fn(func) if func.sig.ident == "main"));
988
989 if has_main {
990 let lines: Vec<&str> = content.lines().collect();
992 let mut insert_line = None;
993 let mut brace_depth: i32 = 0;
994 let mut in_main = false;
995
996 for (i, line) in lines.iter().enumerate() {
997 if line.contains("fn main") {
998 in_main = true;
999 brace_depth = 0;
1000 }
1001 if in_main {
1002 for ch in line.chars() {
1003 if ch == '{' {
1004 brace_depth += 1;
1005 } else if ch == '}' {
1006 brace_depth -= 1;
1007 if brace_depth == 0 {
1008 insert_line = Some(i);
1009 in_main = false;
1010 break;
1011 }
1012 }
1013 }
1014 }
1015 }
1016
1017 if let Some(line_idx) = insert_line {
1018 let mut new_lines: Vec<String> = lines.iter().map(|l| l.to_string()).collect();
1019 new_lines.insert(line_idx, format!(" {}", validate_call));
1020 std::fs::write(build_rs_path, new_lines.join("\n") + "\n")
1021 .context("Failed to write build.rs")?;
1022 return Ok(());
1023 }
1024 }
1025
1026 bail!(
1028 "Could not find fn main() in build.rs. Please add `{}` manually.",
1029 validate_call
1030 );
1031 } else {
1032 let content = format!("fn main() {{\n {}\n}}\n", validate_call);
1034 std::fs::write(build_rs_path, content).context("Failed to create build.rs")?;
1035 }
1036
1037 Ok(())
1038}
1039
1040fn generate_from_local(
1041 local_path: &str,
1042 name: Option<String>,
1043 template: Option<String>,
1044) -> Result<()> {
1045 let local_path = Path::new(local_path);
1046
1047 let manifest_path = local_path.join("Cargo.toml");
1049 let manifest_content = std::fs::read_to_string(&manifest_path)
1050 .with_context(|| format!("Failed to read {}", manifest_path.display()))?;
1051
1052 let crate_name = local_path
1053 .file_name()
1054 .and_then(|s| s.to_str())
1055 .unwrap_or("unknown");
1056 let templates = parse_template_metadata(&manifest_content, crate_name)?;
1057 let template_path = resolve_template(&templates, template.as_deref())?;
1058
1059 generate_from_path(local_path, &template_path, name)
1060}
1061
1062fn generate_from_path(crate_path: &Path, template_path: &str, name: Option<String>) -> Result<()> {
1063 let define = if !std::io::stdout().is_terminal() {
1065 vec!["description=A battery pack for ...".to_string()]
1066 } else {
1067 vec![]
1068 };
1069
1070 let args = GenerateArgs {
1071 template_path: TemplatePath {
1072 path: Some(crate_path.to_string_lossy().into_owned()),
1073 auto_path: Some(template_path.to_string()),
1074 ..Default::default()
1075 },
1076 name,
1077 vcs: Some(Vcs::Git),
1078 define,
1079 ..Default::default()
1080 };
1081
1082 cargo_generate::generate(args)?;
1083
1084 Ok(())
1085}
1086
1087struct CrateMetadata {
1089 version: String,
1090}
1091
1092fn lookup_crate(crate_name: &str) -> Result<CrateMetadata> {
1094 let client = reqwest::blocking::Client::builder()
1095 .user_agent("cargo-bp (https://github.com/battery-pack-rs/battery-pack)")
1096 .build()?;
1097
1098 let url = format!("{}/{}", CRATES_IO_API, crate_name);
1099 let response = client
1100 .get(&url)
1101 .send()
1102 .with_context(|| format!("Failed to query crates.io for '{}'", crate_name))?;
1103
1104 if !response.status().is_success() {
1105 bail!(
1106 "Crate '{}' not found on crates.io (status: {})",
1107 crate_name,
1108 response.status()
1109 );
1110 }
1111
1112 let parsed: CratesIoResponse = response
1113 .json()
1114 .with_context(|| format!("Failed to parse crates.io response for '{}'", crate_name))?;
1115
1116 let version = parsed
1118 .versions
1119 .iter()
1120 .find(|v| !v.yanked)
1121 .map(|v| v.num.clone())
1122 .ok_or_else(|| anyhow::anyhow!("No non-yanked versions found for '{}'", crate_name))?;
1123
1124 Ok(CrateMetadata { version })
1125}
1126
1127fn download_and_extract_crate(crate_name: &str, version: &str) -> Result<tempfile::TempDir> {
1129 let client = reqwest::blocking::Client::builder()
1130 .user_agent("cargo-bp (https://github.com/battery-pack-rs/battery-pack)")
1131 .build()?;
1132
1133 let url = format!(
1135 "{}/{}/{}-{}.crate",
1136 CRATES_IO_CDN, crate_name, crate_name, version
1137 );
1138
1139 let response = client
1140 .get(&url)
1141 .send()
1142 .with_context(|| format!("Failed to download crate from {}", url))?;
1143
1144 if !response.status().is_success() {
1145 bail!(
1146 "Failed to download '{}' version {} (status: {})",
1147 crate_name,
1148 version,
1149 response.status()
1150 );
1151 }
1152
1153 let bytes = response
1154 .bytes()
1155 .with_context(|| "Failed to read crate tarball")?;
1156
1157 let temp_dir = tempfile::tempdir().with_context(|| "Failed to create temp directory")?;
1159
1160 let decoder = GzDecoder::new(&bytes[..]);
1161 let mut archive = Archive::new(decoder);
1162 archive
1163 .unpack(temp_dir.path())
1164 .with_context(|| "Failed to extract crate tarball")?;
1165
1166 Ok(temp_dir)
1167}
1168
1169fn parse_template_metadata(
1170 manifest_content: &str,
1171 crate_name: &str,
1172) -> Result<BTreeMap<String, TemplateConfig>> {
1173 let manifest: CargoManifest =
1174 toml::from_str(manifest_content).with_context(|| "Failed to parse Cargo.toml")?;
1175
1176 let templates = manifest
1177 .package
1178 .and_then(|p| p.metadata)
1179 .and_then(|m| m.battery)
1180 .map(|b| b.templates)
1181 .unwrap_or_default();
1182
1183 if templates.is_empty() {
1184 bail!(
1185 "Battery pack '{}' has no templates defined in [package.metadata.battery.templates]",
1186 crate_name
1187 );
1188 }
1189
1190 Ok(templates)
1191}
1192
1193fn resolve_template(
1194 templates: &BTreeMap<String, TemplateConfig>,
1195 requested: Option<&str>,
1196) -> Result<String> {
1197 match requested {
1198 Some(name) => {
1199 let config = templates.get(name).ok_or_else(|| {
1200 let available: Vec<_> = templates.keys().map(|s| s.as_str()).collect();
1201 anyhow::anyhow!(
1202 "Template '{}' not found. Available templates: {}",
1203 name,
1204 available.join(", ")
1205 )
1206 })?;
1207 Ok(config.path.clone())
1208 }
1209 None => {
1210 if templates.len() == 1 {
1211 let (_, config) = templates.iter().next().unwrap();
1213 Ok(config.path.clone())
1214 } else if let Some(config) = templates.get("default") {
1215 Ok(config.path.clone())
1217 } else {
1218 prompt_for_template(templates)
1220 }
1221 }
1222 }
1223}
1224
1225fn prompt_for_template(templates: &BTreeMap<String, TemplateConfig>) -> Result<String> {
1226 use dialoguer::{Select, theme::ColorfulTheme};
1227
1228 let items: Vec<String> = templates
1230 .iter()
1231 .map(|(name, config)| {
1232 if let Some(desc) = &config.description {
1233 format!("{} - {}", name, desc)
1234 } else {
1235 name.clone()
1236 }
1237 })
1238 .collect();
1239
1240 if !std::io::stdout().is_terminal() {
1242 println!("Available templates:");
1244 for item in &items {
1245 println!(" {}", item);
1246 }
1247 bail!("Multiple templates available. Please specify one with --template <name>");
1248 }
1249
1250 let selection = Select::with_theme(&ColorfulTheme::default())
1252 .with_prompt("Select a template")
1253 .items(&items)
1254 .default(0)
1255 .interact()
1256 .context("Failed to select template")?;
1257
1258 let (_, config) = templates
1260 .iter()
1261 .nth(selection)
1262 .ok_or_else(|| anyhow::anyhow!("Invalid template selection"))?;
1263 Ok(config.path.clone())
1264}
1265
1266pub fn fetch_battery_pack_list(filter: Option<&str>) -> Result<Vec<BatteryPackSummary>> {
1268 let client = reqwest::blocking::Client::builder()
1269 .user_agent("cargo-bp (https://github.com/battery-pack-rs/battery-pack)")
1270 .build()?;
1271
1272 let url = match filter {
1274 Some(q) => format!(
1275 "{CRATES_IO_API}?q={}&keyword=battery-pack&per_page=50",
1276 urlencoding::encode(q)
1277 ),
1278 None => format!("{CRATES_IO_API}?keyword=battery-pack&per_page=50"),
1279 };
1280
1281 let response = client
1282 .get(&url)
1283 .send()
1284 .context("Failed to query crates.io")?;
1285
1286 if !response.status().is_success() {
1287 bail!(
1288 "Failed to list battery packs (status: {})",
1289 response.status()
1290 );
1291 }
1292
1293 let parsed: SearchResponse = response.json().context("Failed to parse response")?;
1294
1295 let battery_packs = parsed
1297 .crates
1298 .into_iter()
1299 .filter(|c| c.name.ends_with("-battery-pack"))
1300 .map(|c| BatteryPackSummary {
1301 short_name: short_name(&c.name).to_string(),
1302 name: c.name,
1303 version: c.max_version,
1304 description: c.description.unwrap_or_default(),
1305 })
1306 .collect();
1307
1308 Ok(battery_packs)
1309}
1310
1311fn print_battery_pack_list(filter: Option<&str>) -> Result<()> {
1312 use console::style;
1313
1314 let battery_packs = fetch_battery_pack_list(filter)?;
1315
1316 if battery_packs.is_empty() {
1317 match filter {
1318 Some(q) => println!("No battery packs found matching '{}'", q),
1319 None => println!("No battery packs found"),
1320 }
1321 return Ok(());
1322 }
1323
1324 let max_name_len = battery_packs
1326 .iter()
1327 .map(|c| c.short_name.len())
1328 .max()
1329 .unwrap_or(0);
1330
1331 let max_version_len = battery_packs
1332 .iter()
1333 .map(|c| c.version.len())
1334 .max()
1335 .unwrap_or(0);
1336
1337 println!();
1338 for bp in &battery_packs {
1339 let desc = bp.description.lines().next().unwrap_or("");
1340
1341 let name_padded = format!("{:<width$}", bp.short_name, width = max_name_len);
1343 let ver_padded = format!("{:<width$}", bp.version, width = max_version_len);
1344
1345 println!(
1346 " {} {} {}",
1347 style(name_padded).green().bold(),
1348 style(ver_padded).dim(),
1349 desc,
1350 );
1351 }
1352 println!();
1353
1354 println!(
1355 "{}",
1356 style(format!("Found {} battery pack(s)", battery_packs.len())).dim()
1357 );
1358
1359 Ok(())
1360}
1361
1362fn short_name(crate_name: &str) -> &str {
1364 crate_name
1365 .strip_suffix("-battery-pack")
1366 .unwrap_or(crate_name)
1367}
1368
1369fn resolve_crate_name(name: &str) -> String {
1372 if name == "battery-pack" || name.ends_with("-battery-pack") {
1373 name.to_string()
1374 } else {
1375 format!("{}-battery-pack", name)
1376 }
1377}
1378
1379pub fn fetch_battery_pack_detail(name: &str, path: Option<&str>) -> Result<BatteryPackDetail> {
1381 if let Some(local_path) = path {
1383 return fetch_battery_pack_detail_from_path(local_path);
1384 }
1385
1386 let crate_name = resolve_crate_name(name);
1387
1388 let crate_info = lookup_crate(&crate_name)?;
1390 let temp_dir = download_and_extract_crate(&crate_name, &crate_info.version)?;
1391 let crate_dir = temp_dir
1392 .path()
1393 .join(format!("{}-{}", crate_name, crate_info.version));
1394
1395 let owners = fetch_owners(&crate_name)?;
1397
1398 build_battery_pack_detail(&crate_dir, crate_name, crate_info.version, owners)
1399}
1400
1401fn fetch_battery_pack_detail_from_path(path: &str) -> Result<BatteryPackDetail> {
1403 let crate_dir = std::path::Path::new(path);
1404
1405 let manifest_path = crate_dir.join("Cargo.toml");
1407 let manifest_content = std::fs::read_to_string(&manifest_path)
1408 .with_context(|| format!("Failed to read {}", manifest_path.display()))?;
1409 let manifest: CargoManifest =
1410 toml::from_str(&manifest_content).with_context(|| "Failed to parse Cargo.toml")?;
1411
1412 let package = manifest.package.unwrap_or_default();
1413 let crate_name = package
1414 .name
1415 .clone()
1416 .unwrap_or_else(|| "unknown".to_string());
1417 let version = package
1418 .version
1419 .clone()
1420 .unwrap_or_else(|| "0.0.0".to_string());
1421
1422 build_battery_pack_detail(
1423 crate_dir,
1424 crate_name,
1425 version,
1426 Vec::new(), )
1428}
1429
1430fn build_battery_pack_detail(
1433 crate_dir: &Path,
1434 crate_name: String,
1435 version: String,
1436 owners: Vec<Owner>,
1437) -> Result<BatteryPackDetail> {
1438 let manifest_path = crate_dir.join("Cargo.toml");
1440 let manifest_content = std::fs::read_to_string(&manifest_path)
1441 .with_context(|| format!("Failed to read {}", manifest_path.display()))?;
1442 let manifest: CargoManifest =
1443 toml::from_str(&manifest_content).with_context(|| "Failed to parse Cargo.toml")?;
1444
1445 let package = manifest.package.unwrap_or_default();
1447 let description = package.description.clone().unwrap_or_default();
1448 let repository = package.repository.clone();
1449 let battery = package.metadata.and_then(|m| m.battery).unwrap_or_default();
1450
1451 let (extends_raw, crates_raw): (Vec<_>, Vec<_>) = manifest
1453 .dependencies
1454 .keys()
1455 .filter(|d| *d != "battery-pack")
1456 .partition(|d| d.ends_with("-battery-pack"));
1457
1458 let extends: Vec<String> = extends_raw
1459 .into_iter()
1460 .map(|d| short_name(d).to_string())
1461 .collect();
1462 let crates: Vec<String> = crates_raw.into_iter().cloned().collect();
1463
1464 let repo_tree = repository.as_ref().and_then(|r| fetch_github_tree(r));
1466
1467 let templates = battery
1469 .templates
1470 .into_iter()
1471 .map(|(name, config)| {
1472 let repo_path = repo_tree
1473 .as_ref()
1474 .and_then(|tree| find_template_path(tree, &config.path));
1475 TemplateInfo {
1476 name,
1477 path: config.path,
1478 description: config.description,
1479 repo_path,
1480 }
1481 })
1482 .collect();
1483
1484 let examples = scan_examples(crate_dir, repo_tree.as_deref());
1486
1487 Ok(BatteryPackDetail {
1488 short_name: short_name(&crate_name).to_string(),
1489 name: crate_name,
1490 version,
1491 description,
1492 repository,
1493 owners: owners.into_iter().map(OwnerInfo::from).collect(),
1494 crates,
1495 extends,
1496 templates,
1497 examples,
1498 })
1499}
1500
1501fn print_battery_pack_detail(name: &str, path: Option<&str>) -> Result<()> {
1502 use console::style;
1503
1504 let detail = fetch_battery_pack_detail(name, path)?;
1505
1506 println!();
1508 println!(
1509 "{} {}",
1510 style(&detail.name).green().bold(),
1511 style(&detail.version).dim()
1512 );
1513 if !detail.description.is_empty() {
1514 println!("{}", detail.description);
1515 }
1516
1517 if !detail.owners.is_empty() {
1519 println!();
1520 println!("{}", style("Authors:").bold());
1521 for owner in &detail.owners {
1522 if let Some(name) = &owner.name {
1523 println!(" {} ({})", name, owner.login);
1524 } else {
1525 println!(" {}", owner.login);
1526 }
1527 }
1528 }
1529
1530 if !detail.crates.is_empty() {
1532 println!();
1533 println!("{}", style("Crates:").bold());
1534 for dep in &detail.crates {
1535 println!(" {}", dep);
1536 }
1537 }
1538
1539 if !detail.extends.is_empty() {
1541 println!();
1542 println!("{}", style("Extends:").bold());
1543 for dep in &detail.extends {
1544 println!(" {}", dep);
1545 }
1546 }
1547
1548 if !detail.templates.is_empty() {
1550 println!();
1551 println!("{}", style("Templates:").bold());
1552 let max_name_len = detail
1553 .templates
1554 .iter()
1555 .map(|t| t.name.len())
1556 .max()
1557 .unwrap_or(0);
1558 for tmpl in &detail.templates {
1559 let name_padded = format!("{:<width$}", tmpl.name, width = max_name_len);
1560 if let Some(desc) = &tmpl.description {
1561 println!(" {} {}", style(name_padded).cyan(), desc);
1562 } else {
1563 println!(" {}", style(name_padded).cyan());
1564 }
1565 }
1566 }
1567
1568 if !detail.examples.is_empty() {
1570 println!();
1571 println!("{}", style("Examples:").bold());
1572 let max_name_len = detail
1573 .examples
1574 .iter()
1575 .map(|e| e.name.len())
1576 .max()
1577 .unwrap_or(0);
1578 for example in &detail.examples {
1579 let name_padded = format!("{:<width$}", example.name, width = max_name_len);
1580 if let Some(desc) = &example.description {
1581 println!(" {} {}", style(name_padded).magenta(), desc);
1582 } else {
1583 println!(" {}", style(name_padded).magenta());
1584 }
1585 }
1586 }
1587
1588 println!();
1590 println!("{}", style("Install:").bold());
1591 println!(" cargo bp add {}", detail.short_name);
1592 println!(" cargo bp new {}", detail.short_name);
1593 println!();
1594
1595 Ok(())
1596}
1597
1598fn fetch_owners(crate_name: &str) -> Result<Vec<Owner>> {
1599 let client = reqwest::blocking::Client::builder()
1600 .user_agent("cargo-bp (https://github.com/battery-pack-rs/battery-pack)")
1601 .build()?;
1602
1603 let url = format!("{}/{}/owners", CRATES_IO_API, crate_name);
1604 let response = client
1605 .get(&url)
1606 .send()
1607 .with_context(|| format!("Failed to fetch owners for '{}'", crate_name))?;
1608
1609 if !response.status().is_success() {
1610 return Ok(Vec::new());
1612 }
1613
1614 let parsed: OwnersResponse = response
1615 .json()
1616 .with_context(|| "Failed to parse owners response")?;
1617
1618 Ok(parsed.users)
1619}
1620
1621fn scan_examples(crate_dir: &std::path::Path, repo_tree: Option<&[String]>) -> Vec<ExampleInfo> {
1624 let examples_dir = crate_dir.join("examples");
1625 if !examples_dir.exists() {
1626 return Vec::new();
1627 }
1628
1629 let mut examples = Vec::new();
1630
1631 if let Ok(entries) = std::fs::read_dir(&examples_dir) {
1632 for entry in entries.flatten() {
1633 let path = entry.path();
1634 if path.extension().is_some_and(|ext| ext == "rs") {
1635 if let Some(name) = path.file_stem().and_then(|s| s.to_str()) {
1636 let description = extract_example_description(&path);
1637 let repo_path = repo_tree.and_then(|tree| find_example_path(tree, name));
1638 examples.push(ExampleInfo {
1639 name: name.to_string(),
1640 description,
1641 repo_path,
1642 });
1643 }
1644 }
1645 }
1646 }
1647
1648 examples.sort_by(|a, b| a.name.cmp(&b.name));
1650 examples
1651}
1652
1653fn extract_example_description(path: &std::path::Path) -> Option<String> {
1655 let content = std::fs::read_to_string(path).ok()?;
1656
1657 for line in content.lines() {
1659 let trimmed = line.trim();
1660 if trimmed.starts_with("//!") {
1661 let desc = trimmed.strip_prefix("//!").unwrap_or("").trim();
1662 if !desc.is_empty() {
1663 return Some(desc.to_string());
1664 }
1665 } else if !trimmed.is_empty() && !trimmed.starts_with("//") {
1666 break;
1668 }
1669 }
1670 None
1671}
1672
1673fn fetch_github_tree(repository: &str) -> Option<Vec<String>> {
1676 let gh_path = repository
1678 .strip_prefix("https://github.com/")
1679 .or_else(|| repository.strip_prefix("http://github.com/"))?;
1680 let gh_path = gh_path.strip_suffix(".git").unwrap_or(gh_path);
1681 let gh_path = gh_path.trim_end_matches('/');
1682
1683 let client = reqwest::blocking::Client::builder()
1684 .user_agent("cargo-bp (https://github.com/battery-pack-rs/battery-pack)")
1685 .build()
1686 .ok()?;
1687
1688 let url = format!(
1690 "https://api.github.com/repos/{}/git/trees/main?recursive=1",
1691 gh_path
1692 );
1693
1694 let response = client.get(&url).send().ok()?;
1695 if !response.status().is_success() {
1696 return None;
1697 }
1698
1699 let tree_response: GitHubTreeResponse = response.json().ok()?;
1700
1701 Some(tree_response.tree.into_iter().map(|e| e.path).collect())
1703}
1704
1705fn find_example_path(tree: &[String], example_name: &str) -> Option<String> {
1708 let suffix = format!("examples/{}.rs", example_name);
1709 tree.iter().find(|path| path.ends_with(&suffix)).cloned()
1710}
1711
1712fn find_template_path(tree: &[String], template_path: &str) -> Option<String> {
1715 tree.iter()
1717 .find(|path| path.ends_with(template_path))
1718 .cloned()
1719}