diff --git a/.github/workflows/update_changelog.yml b/.github/workflows/update-changelog.yml similarity index 100% rename from .github/workflows/update_changelog.yml rename to .github/workflows/update-changelog.yml diff --git a/Cargo.toml b/Cargo.toml index 7cb3411..f2075b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,4 +11,4 @@ reqwest = { version = "0.11", default-features = false, features = ["json", "gzi serde_json = "1.0.140" syntect = "5.2.0" openssl = { version = "0.10", features = ["vendored"] } -indicatif = "0.17.12" +indicatif = "0.17.11" diff --git a/scripts/install_issue_template.sh b/scripts/install_issue_template.sh deleted file mode 100644 index b5204ca..0000000 --- a/scripts/install_issue_template.sh +++ /dev/null @@ -1,73 +0,0 @@ -# ----------------------------------------------------------------------------- -# install_issue_template.sh -# -# This script automates the installation of GitHub issue templates into a local -# repository. It downloads a predefined set of issue template YAML files from a -# remote GitHub repository and places them in the `.github/ISSUE_TEMPLATE` -# directory of the current project. -# -# Usage: -# ./install_issue_template.sh -# -# Features: -# - Creates the `.github/ISSUE_TEMPLATE` directory if it does not exist. -# - Downloads multiple issue template files and a configuration file from a -# specified remote repository. -# - Ensures robust execution with strict error handling (`set -euo pipefail`). -# -# Requirements: -# - curl: Command-line tool for downloading files. -# -# Environment Variables: -# REPO_BASE_URL: Base URL of the remote repository containing the templates. -# -# Output: -# - Issue template files are saved to `.github/ISSUE_TEMPLATE/`. -# - Informational messages are printed to the console. -# -# Author: rafaeljohn9 -# ----------------------------------------------------------------------------- -#!/usr/bin/env bash - -set -euo pipefail - -REPO_BASE_URL="https://raw.githubusercontent.com/RafaelJohn9/gh-templates/main" - -TEMPLATE_DIR=".github/ISSUE_TEMPLATE" -ALL_FILES=( - "01-bug.yml" - "02-feature-request.yml" - "03-documentation.yml" - "04-community-collaboration.yml" - "05-developer-experience-feedback.yml" - "06-support-question.yml" - "07-test.yml" -) - -# If arguments are provided, use them as the list of files to download -if [[ $# -gt 0 ]]; then - FILES=() - for arg in "$@"; do - if [[ " ${ALL_FILES[*]} " == *" $arg "* ]]; then - FILES+=("$arg") - else - echo "⚠️ Warning: '$arg' is not a recognized template. Skipping." - fi - done - if [[ ${#FILES[@]} -eq 0 ]]; then - echo "❌ No valid templates specified. Exiting." - exit 1 - fi -else - FILES=("${ALL_FILES[@]}") -fi - -echo "📁 Creating template directory: $TEMPLATE_DIR" -mkdir -p "$TEMPLATE_DIR" - -for file in "${FILES[@]}"; do - echo "⬇️ Downloading $file" - curl -sSfL "$REPO_BASE_URL/templates/ISSUE_TEMPLATE/$file" -o "$TEMPLATE_DIR/$file" -done - -echo "✅ GitHub issue templates installed to '$TEMPLATE_DIR'." diff --git a/src/commands/issue/add.rs b/src/commands/issue/add.rs new file mode 100644 index 0000000..b88a3d6 --- /dev/null +++ b/src/commands/issue/add.rs @@ -0,0 +1,81 @@ +use std::path::Path; +use std::time::Duration; + +use indicatif::{ProgressBar, ProgressStyle}; + +use super::{GITHUB_API_BASE, GITHUB_RAW_BASE}; +use crate::utils::remote::Fetcher; + +const OUTPUT_BASE_PATH: &str = ".github"; +const OUTPUT: &str = "ISSUE_TEMPLATE"; + +pub fn add(args: &[String]) -> anyhow::Result<()> { + let fetcher = Fetcher::new(); + + if args.is_empty() { + return Err(anyhow::anyhow!( + "No issue template specified. Use --all to download all templates." + )); + } + + // Check if --all flag is provided + if args.contains(&"--all".to_string()) { + let pb = ProgressBar::new_spinner(); + pb.set_style( + ProgressStyle::default_spinner() + .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]) + .template("{spinner} {msg}") + .unwrap(), + ); + pb.enable_steady_tick(Duration::from_millis(100)); + pb.set_message("Fetching all templates..."); + + let url = format!("{}/issue-templates", GITHUB_API_BASE); + let entries = fetcher.fetch_json(&url)?; + let mut downloaded_templates = Vec::new(); + + if let Some(array) = entries.as_array() { + for entry in array { + if let Some(name) = entry.get("name").and_then(|n| n.as_str()) { + let template_name = match name.rfind('.') { + Some(idx) => &name[..idx], + None => name, + }; + + pb.set_message(format!("Downloading template: {}", template_name)); + + let url = format!("{}/issue-templates/{}", GITHUB_RAW_BASE, name); + let dest_path = Path::new(OUTPUT_BASE_PATH).join(OUTPUT).join(name); + + fetcher.fetch_to_file(&url, &dest_path)?; + downloaded_templates.push(format!("{}.yml", template_name)); + } + } + } + pb.finish_and_clear(); + println!( + "\x1b[32m✓\x1b[0m Downloaded all issue templates to {}/{}", + OUTPUT_BASE_PATH, OUTPUT + ); + for template in downloaded_templates { + println!(" \x1b[32m>\x1b[0m {}", template); + } + } else { + // Download specified templates + for template in args { + let url = format!("{}/issue-templates/{}.yml", GITHUB_RAW_BASE, template); + let dest_path = Path::new(OUTPUT_BASE_PATH) + .join(OUTPUT) + .join(format!("{}.yml", template)); + + fetcher.fetch_to_file(&url, &dest_path)?; + + println!( + "\x1b[32m✓\x1b[0m Downloaded and added issue template: {}", + dest_path.display() + ); + } + } + + Ok(()) +} diff --git a/src/commands/issue.rs b/src/commands/issue/list.rs similarity index 64% rename from src/commands/issue.rs rename to src/commands/issue/list.rs index fe15f48..fecc99e 100644 --- a/src/commands/issue.rs +++ b/src/commands/issue/list.rs @@ -1,36 +1,12 @@ -use std::path::Path; use std::time::Duration; use indicatif::{ProgressBar, ProgressStyle}; +use super::{GITHUB_API_BASE, GITHUB_RAW_BASE}; use crate::utils::get_comment; -use crate::utils::pretty_print; use crate::utils::remote::Fetcher; -const OUTPUT_BASE_PATH: &str = ".github"; -const OUTPUT: &str = "ISSUE_TEMPLATE"; -const GITHUB_RAW_BASE: &str = - "https://raw.githubusercontent.com/rafaeljohn9/gh-templates/main/templates"; -const GITHUB_API_BASE: &str = - "https://api.github.com/repos/rafaeljohn9/gh-templates/contents/templates"; - -pub fn add(template: &str) -> anyhow::Result<()> { - let fetcher = Fetcher::new(); - let url = format!("{}/issue-templates/{}.yml", GITHUB_RAW_BASE, template); - let dest_path = Path::new(OUTPUT_BASE_PATH) - .join(OUTPUT) - .join(format!("{}.yml", template)); - - fetcher.fetch_to_file(&url, &dest_path)?; - - println!( - "\x1b[32m✓\x1b[0m Downloaded and added issue template: {}", - dest_path.display() - ); - Ok(()) -} - -pub fn list() -> anyhow::Result<()> { +pub fn list(_extra_args: &[String]) -> anyhow::Result<()> { let fetcher = Fetcher::new(); let pb = ProgressBar::new_spinner(); @@ -87,14 +63,3 @@ pub fn list() -> anyhow::Result<()> { } Ok(()) } - -pub fn preview(template: &str) -> anyhow::Result<()> { - let fetcher = Fetcher::new(); - let url = format!("{}/issue-templates/{}.yml", GITHUB_RAW_BASE, template); - - println!("\x1b[32m✓\x1b[0m Previewing issue template: {}", template); - - let content = fetcher.fetch_content(&url)?; - pretty_print::print_highlighted("yml", &content); - Ok(()) -} diff --git a/src/commands/issue/mod.rs b/src/commands/issue/mod.rs new file mode 100644 index 0000000..86b1aad --- /dev/null +++ b/src/commands/issue/mod.rs @@ -0,0 +1,16 @@ +// Global constants - these can stay in the main module file +const GITHUB_RAW_BASE: &str = + "https://raw.githubusercontent.com/rafaeljohn9/gh-templates/main/templates"; +const GITHUB_API_BASE: &str = + "https://api.github.com/repos/rafaeljohn9/gh-templates/contents/templates"; + +// Re-export submodules +pub mod add; +pub mod list; +pub mod preview; + +// Re-export the main functions for backward compatibility +pub use add::add; +pub use list::list; +pub use preview::preview; + diff --git a/src/commands/issue/preview.rs b/src/commands/issue/preview.rs new file mode 100644 index 0000000..d2cc63b --- /dev/null +++ b/src/commands/issue/preview.rs @@ -0,0 +1,14 @@ +use super::GITHUB_RAW_BASE; +use crate::utils::pretty_print; +use crate::utils::remote::Fetcher; + +pub fn preview(template: &str, _extra_args: &[String]) -> anyhow::Result<()> { + let fetcher = Fetcher::new(); + let url = format!("{}/issue-templates/{}.yml", GITHUB_RAW_BASE, template); + + println!("\x1b[32m✓\x1b[0m Previewing issue template: {}", template); + + let content = fetcher.fetch_content(&url)?; + pretty_print::print_highlighted("yml", &content); + Ok(()) +} diff --git a/src/commands/license/add.rs b/src/commands/license/add.rs new file mode 100644 index 0000000..653c3f8 --- /dev/null +++ b/src/commands/license/add.rs @@ -0,0 +1,36 @@ +use crate::utils::remote::Fetcher; +use anyhow::anyhow; +use std::path::Path; + +use super::GITHUB_LICENSES_API; + +pub fn add(args: &[String]) -> anyhow::Result<()> { + if args.is_empty() { + return Err(anyhow!("At least one license ID is required")); + } + + let fetcher = Fetcher::new(); + + for id in args { + let url = format!("{}/{}", GITHUB_LICENSES_API, id.to_lowercase()); + + let license_data = fetcher.fetch_json(&url)?; + + let body = license_data + .get("body") + .and_then(|b| b.as_str()) + .ok_or_else(|| anyhow!("License body not found for {}", id))?; + + let filename = format!("LICENSE.{}", id.to_uppercase()); + let dest_path = Path::new(&filename); + + std::fs::write(dest_path, body)?; + + println!( + "\x1b[32m✓\x1b[0m Downloaded and added license: {}", + dest_path.display() + ); + } + + Ok(()) +} diff --git a/src/commands/license/list.rs b/src/commands/license/list.rs new file mode 100644 index 0000000..7e3b559 --- /dev/null +++ b/src/commands/license/list.rs @@ -0,0 +1,39 @@ +use crate::utils::remote::Fetcher; +use indicatif::{ProgressBar, ProgressStyle}; +use std::time::Duration; + +use super::GITHUB_LICENSES_API; + +pub fn list(_extra_args: &[String]) -> anyhow::Result<()> { + let fetcher = Fetcher::new(); + + let pb = ProgressBar::new_spinner(); + pb.set_style( + ProgressStyle::default_spinner() + .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]) + .template("{spinner} {msg}") + .unwrap(), + ); + pb.enable_steady_tick(Duration::from_millis(100)); + pb.set_message("Fetching license list..."); + + let licenses = fetcher.fetch_json(GITHUB_LICENSES_API)?; + + pb.finish_with_message("Successfully fetched licenses"); + + if let Some(array) = licenses.as_array() { + println!("\x1b[32m✓\x1b[0m Available licenses:"); + for license in array { + if let (Some(key), Some(name)) = ( + license.get("key").and_then(|k| k.as_str()), + license.get("name").and_then(|n| n.as_str()), + ) { + println!(" \x1b[32m>\x1b[0m {:<15} {}", key, name); + } + } + } else { + println!("No licenses found."); + } + + Ok(()) +} diff --git a/src/commands/license/mod.rs b/src/commands/license/mod.rs new file mode 100644 index 0000000..df11e39 --- /dev/null +++ b/src/commands/license/mod.rs @@ -0,0 +1,12 @@ +// Global constants - these can stay in the main module file +pub const GITHUB_LICENSES_API: &str = "https://api.github.com/licenses"; + +// Re-export submodules +pub mod add; +pub mod list; +pub mod preview; + +// Re-export the main functions for backward compatibility +pub use add::add; +pub use list::list; +pub use preview::preview; diff --git a/src/commands/license/preview.rs b/src/commands/license/preview.rs new file mode 100644 index 0000000..7a6ab08 --- /dev/null +++ b/src/commands/license/preview.rs @@ -0,0 +1,139 @@ +use crate::utils::remote::Fetcher; +use anyhow::anyhow; + +use super::GITHUB_LICENSES_API; + +pub fn preview(id: &str, extra_args: &[String]) -> anyhow::Result<()> { + let fetcher = Fetcher::new(); + let url = format!("{}/{}", GITHUB_LICENSES_API, id.to_lowercase()); + + let license_data = fetcher.fetch_json(&url)?; + + let name = license_data + .get("name") + .and_then(|n| n.as_str()) + .unwrap_or("Unknown License"); + + println!("\x1b[36mLicense Name:\x1b[0m {}\n", name); + + // Parse extra arguments + let mut description = false; + let mut permissions = false; + let mut limitations = false; + let mut conditions = false; + let mut details = false; + + for arg in extra_args { + let arg_str = arg.as_str(); + if arg_str.starts_with('-') && !arg_str.starts_with("--") { + // Handle combined short flags like -pdl + for c in arg_str.chars().skip(1) { + match c { + 'd' => description = true, + 'p' => permissions = true, + 'l' => limitations = true, + 'c' => conditions = true, + 'a' => details = true, + _ => { + return Err(anyhow!("Unknown flag: -{}", c)); + } + } + } + } else { + match arg_str { + "--description" | "-d" => description = true, + "--permissions" | "-p" => permissions = true, + "--limitations" | "-l" => limitations = true, + "--conditions" | "-c" => conditions = true, + "--details" | "-a" => details = true, + _ => { + return Err(anyhow!("Unknown argument: {}", arg)); + } + } + } + } + + if description { + if let Some(desc) = license_data.get("description").and_then(|d| d.as_str()) { + println!("\x1b[36mDescription:\x1b[0m"); + println!("{}", desc); + println!(); + } + } + + if permissions || details { + if let Some(perms) = license_data.get("permissions").and_then(|p| p.as_array()) { + println!("\x1b[32mPermissions:\x1b[0m"); + for perm in perms { + if let Some(perm_str) = perm.as_str() { + println!(" ✓ {}", format_permission(perm_str)); + } + } + println!(); + } + } + + if limitations || details { + if let Some(limits) = license_data.get("limitations").and_then(|l| l.as_array()) { + println!("\x1b[31mLimitations:\x1b[0m"); + for limit in limits { + if let Some(limit_str) = limit.as_str() { + println!(" ✗ {}", format_limitation(limit_str)); + } + } + println!(); + } + } + + if conditions || details { + if let Some(conds) = license_data.get("conditions").and_then(|c| c.as_array()) { + println!("\x1b[33mConditions:\x1b[0m"); + for condition in conds { + if let Some(cond_str) = condition.as_str() { + println!(" ! {}", format_condition(cond_str)); + } + } + println!(); + } + } + + // If no specific flags are set, show the full license text + if !permissions && !limitations && !conditions && !description && !details { + if let Some(body) = license_data.get("body").and_then(|b| b.as_str()) { + println!("\x1b[36mLicense Text:\x1b[0m"); + println!("{}", body); + } + } + + Ok(()) +} + +fn format_permission(perm: &str) -> String { + match perm { + "commercial-use" => "Commercial use".to_string(), + "modifications" => "Modify".to_string(), + "distribution" => "Distribute".to_string(), + "patent-use" => "Patent use".to_string(), + "private-use" => "Private use".to_string(), + _ => perm.replace('-', " "), + } +} + +fn format_limitation(limit: &str) -> String { + match limit { + "liability" => "Liability".to_string(), + "warranty" => "Warranty".to_string(), + "trademark-use" => "Trademark use".to_string(), + _ => limit.replace('-', " "), + } +} + +fn format_condition(cond: &str) -> String { + match cond { + "include-copyright" => "License and copyright notice".to_string(), + "document-changes" => "State changes".to_string(), + "disclose-source" => "Disclose source".to_string(), + "same-license" => "Same license".to_string(), + _ => cond.replace('-', " "), + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index ed89e93..3e74f6b 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,22 +1,30 @@ pub mod issue; +pub mod license; -pub fn dispatch_add(category: &str, template: &str) -> anyhow::Result<()> { +pub fn dispatch_add(category: &str, args: &[String]) -> anyhow::Result<()> { match category { - "issue" => issue::add(template), + "issue" => issue::add(args), + "license" => license::add(args), _ => Err(anyhow::anyhow!("Unknown category: {}", category)), } } -pub fn dispatch_list(category: &str) -> anyhow::Result<()> { +pub fn dispatch_list(category: &str, extra_args: &[String]) -> anyhow::Result<()> { match category { - "issue" => issue::list(), + "issue" => issue::list(extra_args), + "license" => license::list(extra_args), _ => Err(anyhow::anyhow!("Unknown category: {}", category)), } } -pub fn dispatch_preview(category: &str, template: &str) -> anyhow::Result<()> { +pub fn dispatch_preview( + category: &str, + template: &str, + extra_args: &[String], +) -> anyhow::Result<()> { match category { - "issue" => issue::preview(template), + "issue" => issue::preview(template, extra_args), + "license" => license::preview(template, extra_args), _ => Err(anyhow::anyhow!("Unknown category: {}", category)), } } diff --git a/src/main.rs b/src/main.rs index 4d5a23a..9d871ab 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,14 +15,17 @@ enum Commands { Add { /// Template category (e.g., issue, pr, ci, license, gitignore) category: String, - /// Template name (e.g., bug, feature-request) - template: String, + /// Template name(s) (e.g., bug, feature-request) + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + args: Vec, }, /// List available templates in a category List { /// Category to list (e.g., issue, pr, ci, license, gitignore) category: String, + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + extra_args: Vec, }, /// Preview a template in a category @@ -31,23 +34,31 @@ enum Commands { category: String, /// Template name to preview (e.g., bug, feature-request) template: String, + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + extra_args: Vec, }, } - fn main() -> anyhow::Result<()> { let cli = Cli::parse(); match cli.command { - Commands::Add { category, template } => { - commands::dispatch_add(&category, &template)?; + Commands::Add { category, args } => { + commands::dispatch_add(&category, &args)?; } - Commands::List { category } => { - commands::dispatch_list(&category)?; + Commands::List { + category, + extra_args, + } => { + commands::dispatch_list(&category, &extra_args)?; } - Commands::Preview { category, template } => { - commands::dispatch_preview(&category, &template)?; + Commands::Preview { + category, + template, + extra_args, + } => { + commands::dispatch_preview(&category, &template, &extra_args)?; } } diff --git a/src/utils/remote.rs b/src/utils/remote.rs index 052a8e3..50a7177 100644 --- a/src/utils/remote.rs +++ b/src/utils/remote.rs @@ -78,6 +78,7 @@ impl Fetcher { } /// Download binary content and save to file + #[allow(dead_code)] pub fn download_to_file(&self, url: &str, output_path: &Path) -> anyhow::Result<()> { let response = self .client