commit 6dc747f8b0518802045cb8cc5347169e5632668f Author: kjuulh Date: Sat Dec 17 21:51:41 2022 +0100 add tldr and base diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..f141b59 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,414 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cc" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d63b9e9c07271b9957ad22c173bae2a4d9a81127680962039296abcd2f8251d" +dependencies = [ + "bitflags", + "clap_lex", + "is-terminal", + "once_cell", + "strsim", + "termcolor", +] + +[[package]] +name = "clap_lex" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "eyre" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "io-lifetimes" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "is-terminal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927609f78c2913a6f6ac3c27a4fe87f43e2a35367c0c4b0f8265e8f49a104330" +dependencies = [ + "hermit-abi", + "io-lifetimes", + "rustix", + "windows-sys", +] + +[[package]] +name = "libc" +version = "0.2.138" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8" + +[[package]] +name = "linux-raw-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" + +[[package]] +name = "once_cell" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" + +[[package]] +name = "os_str_bytes" +version = "6.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" + +[[package]] +name = "proc-macro2" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall", + "thiserror", +] + +[[package]] +name = "rustix" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3807b5d10909833d3e9acd1eb5fb988f79376ff10fce42937de71a449c4c588" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tldr" +version = "0.1.0" +dependencies = [ + "clap", + "dirs", + "eyre", + "util", + "walkdir", +] + +[[package]] +name = "toolkit" +version = "0.1.0" +dependencies = [ + "clap", + "eyre", + "tldr", + "util", +] + +[[package]] +name = "unicode-ident" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" + +[[package]] +name = "util" +version = "0.1.0" +dependencies = [ + "clap", + "eyre", +] + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..cd5e653 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "toolkit" +version = "0.1.0" +edition = "2021" + +[workspace] +members = ["crates/tldr", "crates/util"] + +[workspace.dependencies] +clap = { version = "4.0.29", features = ["cargo"] } +eyre = "0.6.8" +dirs = "4.0.0" +walkdir = "2.3.2" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +tldr = { path = "crates/tldr" } +util = { path = "crates/util" } + +clap.workspace = true +eyre.workspace = true diff --git a/crates/tldr/.gitignore b/crates/tldr/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/crates/tldr/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/crates/tldr/Cargo.toml b/crates/tldr/Cargo.toml new file mode 100644 index 0000000..d60ed12 --- /dev/null +++ b/crates/tldr/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "tldr" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +util = { path = "../util" } + +eyre.workspace = true +clap.workspace = true +dirs.workspace = true +walkdir.workspace = true diff --git a/crates/tldr/src/lib.rs b/crates/tldr/src/lib.rs new file mode 100644 index 0000000..ba847eb --- /dev/null +++ b/crates/tldr/src/lib.rs @@ -0,0 +1,86 @@ +pub(crate) mod update; + +pub struct Tldr; + +impl Tldr { + fn run() -> eyre::Result<()> { + let cache_dir = + dirs::cache_dir().ok_or(eyre::anyhow!("could not find a valid cache dir"))?; + + let mut tldr_cache_dir = cache_dir.clone(); + tldr_cache_dir.push("kah-toolkit/tldr/store/"); + + if !tldr_cache_dir.exists() { + return Err(eyre::anyhow!("you need to run first")); + } + + let mut tldr_pages_path = tldr_cache_dir.clone(); + tldr_pages_path.push("pages"); + + if !tldr_pages_path.exists() { + return Err(eyre::anyhow!("you need to run first")); + } + + let mut entries: Vec = Vec::new(); + for entry in walkdir::WalkDir::new(&tldr_pages_path) { + let entry = entry?; + + let path = entry.path().to_path_buf(); + + match path.extension() { + None => continue, + Some(ext) => { + if ext != "md" { + continue; + } + } + } + + let parent_str = path + .parent() + .ok_or(eyre::anyhow!("could not find parent for file"))? + .to_string_lossy(); + + let parent = parent_str.split("/").last(); + let file_name = entry.file_name(); + + entries.push(format!( + "{}/{}", + parent.ok_or(eyre::anyhow!("path contains non ascii characters"))?, + file_name + .to_str() + .ok_or(eyre::anyhow!("path contains non ascii characters"))?, + )) + } + + let paths = entries.join("\n"); + + let output = util::shell::run_with_input_and_output(&["fzf"], paths)?; + + let choice = std::str::from_utf8(output.stdout.as_slice())?.trim(); + + let mut tldr_choice_path = tldr_pages_path; + tldr_choice_path.push(choice); + + let contents = std::fs::read_to_string(tldr_choice_path)?; + + util::shell::run_with_input(&["glow", "-"], contents)?; + + Ok(()) + } +} + +impl util::Cmd for Tldr { + fn cmd() -> eyre::Result { + let cmd = clap::Command::new("tldr").subcommands([update::Update::cmd()?]); + + Ok(cmd) + } + + fn exec(args: &clap::ArgMatches) -> eyre::Result<()> { + match args.subcommand() { + Some(("update", subcmd)) => update::Update::exec(subcmd), + _ => Tldr::run(), + } + } +} diff --git a/crates/tldr/src/update.rs b/crates/tldr/src/update.rs new file mode 100644 index 0000000..b8e1cb5 --- /dev/null +++ b/crates/tldr/src/update.rs @@ -0,0 +1,32 @@ +pub struct Update; + +impl util::Cmd for Update { + fn cmd() -> eyre::Result { + Ok(clap::Command::new("update")) + } + + fn exec(_: &clap::ArgMatches) -> eyre::Result<()> { + let cache_dir = + dirs::cache_dir().ok_or(eyre::anyhow!("could not find a valid cache dir"))?; + + let mut tldr_cache_dir = cache_dir.clone(); + tldr_cache_dir.push("kah-toolkit/tldr/store/"); + + std::fs::remove_dir_all(&tldr_cache_dir)?; + std::fs::create_dir_all(&tldr_cache_dir)?; + + util::shell::run( + format!( + "gh repo clone tldr-pages/tldr {}", + &tldr_cache_dir + .to_str() + .ok_or(eyre::anyhow!("pathstring contains non ascii-characters"))? + ) + .split(" ") + .collect::>() + .as_slice(), + )?; + + Ok(()) + } +} diff --git a/crates/util/Cargo.toml b/crates/util/Cargo.toml new file mode 100644 index 0000000..71008a5 --- /dev/null +++ b/crates/util/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "util" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +eyre.workspace = true +clap.workspace = true diff --git a/crates/util/src/lib.rs b/crates/util/src/lib.rs new file mode 100644 index 0000000..e74b8be --- /dev/null +++ b/crates/util/src/lib.rs @@ -0,0 +1,7 @@ +pub mod shell; + +pub trait Cmd { + fn cmd() -> eyre::Result; + fn exec(args: &clap::ArgMatches) -> eyre::Result<()>; +} + diff --git a/crates/util/src/shell.rs b/crates/util/src/shell.rs new file mode 100644 index 0000000..3e9a021 --- /dev/null +++ b/crates/util/src/shell.rs @@ -0,0 +1,123 @@ +use std::io::Write; + +pub fn run(args: &[&str]) -> eyre::Result<()> { + let output = std::process::Command::new( + args.first() + .ok_or(eyre::anyhow!("could not find first arg"))?, + ) + .args( + args.to_vec() + .into_iter() + .skip(1) + .collect::>() + .as_slice(), + ) + .stdout(std::process::Stdio::inherit()) + .stderr(std::process::Stdio::inherit()) + .stdin(std::process::Stdio::inherit()) + .output(); + + match output { + Ok(o) => { + if o.status.success() { + Ok(()) + } else { + Err(eyre::anyhow!( + "command failed with statuscode: {}", + o.status + .code() + .ok_or(eyre::anyhow!("could not get a status code from process"))? + )) + } + } + Err(e) => Err(eyre::anyhow!(e)), + } +} +pub fn run_with_input(args: &[&str], input: String) -> eyre::Result<()> { + let output = std::process::Command::new( + args.first() + .ok_or(eyre::anyhow!("could not find first arg"))?, + ) + .args( + args.to_vec() + .into_iter() + .skip(1) + .collect::>() + .as_slice(), + ) + .stdout(std::process::Stdio::inherit()) + .stderr(std::process::Stdio::inherit()) + .stdin(std::process::Stdio::piped()) + .spawn(); + + match output { + Ok(mut o) => { + let stdin = o + .stdin + .as_mut() + .ok_or(eyre::anyhow!("could not acquire stdin"))?; + + stdin.write_all(input.as_bytes())?; + drop(stdin); + + let o = o.wait_with_output()?; + if o.status.success() { + Ok(()) + } else { + Err(eyre::anyhow!( + "command failed with statuscode: {}", + o.status + .code() + .ok_or(eyre::anyhow!("could not get a status code from process"))? + )) + } + } + Err(e) => Err(eyre::anyhow!(e)), + } +} + +pub fn run_with_input_and_output( + args: &[&str], + input: String, +) -> eyre::Result { + let output = std::process::Command::new( + args.first() + .ok_or(eyre::anyhow!("could not find first arg"))?, + ) + .args( + args.to_vec() + .into_iter() + .skip(1) + .collect::>() + .as_slice(), + ) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::inherit()) + .stdin(std::process::Stdio::piped()) + .spawn(); + + match output { + Ok(mut o) => { + let stdin = o + .stdin + .as_mut() + .ok_or(eyre::anyhow!("could not acquire stdin"))?; + + stdin.write_all(input.as_bytes())?; + drop(stdin); + + let o = o.wait_with_output()?; + if o.status.success() { + Ok(o) + } else { + Err(eyre::anyhow!( + "command failed with statuscode: {}", + o.status + .code() + .ok_or(eyre::anyhow!("could not get a status code from process"))? + )) + } + } + Err(e) => Err(eyre::anyhow!(e)), + } +} diff --git a/output.gif b/output.gif new file mode 100644 index 0000000..90a5002 Binary files /dev/null and b/output.gif differ diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..cb1f63b --- /dev/null +++ b/src/main.rs @@ -0,0 +1,16 @@ +use prereqs::prereqs_exec; +use util::Cmd; + +mod prereqs; + +fn main() -> eyre::Result<()> { + let matches = clap::Command::new("toolkit") + .subcommands([prereqs::prereqs()?, tldr::Tldr::cmd()?]) + .get_matches(); + + match matches.subcommand() { + Some(("prereqs", subcmd)) => prereqs_exec(subcmd), + Some(("tldr", subcmd)) => tldr::Tldr::exec(subcmd), + _ => Err(eyre::anyhow!("no command selected!")), + } +} diff --git a/src/prereqs.rs b/src/prereqs.rs new file mode 100644 index 0000000..1190097 --- /dev/null +++ b/src/prereqs.rs @@ -0,0 +1,37 @@ +use clap::ArgMatches; + +const DEPS: &[&str] = &["gh", "fzf"]; + +// -- List +// + +pub fn ls() -> clap::Command { + clap::Command::new("ls") +} + +pub fn ls_exec() -> eyre::Result<()> { + println!("Required packages\n---"); + + let mut deps: Vec<&str> = DEPS.into_iter().map(|d| d.clone()).collect(); + deps.sort(); + for dep in deps { + println!("{}", dep) + } + Ok(()) +} + +// -- Prereqs +// + +pub fn prereqs() -> eyre::Result { + let cmd = clap::Command::new("prereqs").subcommands([ls()]); + + Ok(cmd) +} + +pub fn prereqs_exec(matches: &ArgMatches) -> eyre::Result<()> { + match matches.subcommand() { + Some(("ls", _)) => ls_exec(), + _ => Err(eyre::anyhow!("not implemented")), + } +}