From a5755149ec43b5c5b1b17e636306f8d7404b9ff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matheus=20Bregu=C3=AAz?= Date: Mon, 16 Jun 2025 19:58:56 -0300 Subject: [PATCH] Add completions command and update TODO --- Cargo.lock | 10 +++ Cargo.toml | 1 + design/CLI_REFACTOR_PLAN.md | 99 ++++++++++++++++++++++++ design/TODO.md | 14 ++++ src/cli/commands/cache.rs | 58 ++++++++++++++ src/cli/commands/completions.rs | 10 +++ src/cli/commands/config.rs | 51 ++++++++++++ src/cli/commands/migrate.rs | 12 ++- src/cli/commands/mod.rs | 13 +++- src/cli/commands/plugin.rs | 18 +++++ src/cli/commands/theme.rs | 18 +++++ src/cli/logging.rs | 2 +- src/cli/mod.rs | 24 ++++-- src/cli/types.rs | 133 ++++++++++++++++++++++++++------ src/migrate/mod.rs | 12 ++- 15 files changed, 439 insertions(+), 36 deletions(-) create mode 100644 design/CLI_REFACTOR_PLAN.md create mode 100644 design/TODO.md create mode 100644 src/cli/commands/cache.rs create mode 100644 src/cli/commands/completions.rs create mode 100644 src/cli/commands/config.rs create mode 100644 src/cli/commands/plugin.rs create mode 100644 src/cli/commands/theme.rs diff --git a/Cargo.lock b/Cargo.lock index 972bfe2..0d9fe3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -497,6 +497,15 @@ dependencies = [ "terminal_size", ] +[[package]] +name = "clap_complete" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aad5b1b4de04fead402672b48897030eec1f3bfe1550776322f59f6d6e6a5677" +dependencies = [ + "clap", +] + [[package]] name = "clap_derive" version = "4.5.32" @@ -1848,6 +1857,7 @@ dependencies = [ "axum-server", "chrono", "clap", + "clap_complete", "comrak", "csv", "frontmatter", diff --git a/Cargo.toml b/Cargo.toml index 527b35a..9f36c5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ tls = ["dep:tokio-rustls"] [dependencies] clap = { version = "4.4", features = ["derive"] } +clap_complete = "4.4" liquid = "0.26" liquid-core = "0.26" liquid-lib = { version = "0.26", features = ["stdlib"] } diff --git a/design/CLI_REFACTOR_PLAN.md b/design/CLI_REFACTOR_PLAN.md new file mode 100644 index 0000000..ec81593 --- /dev/null +++ b/design/CLI_REFACTOR_PLAN.md @@ -0,0 +1,99 @@ +# Plano de Refatoração e Expansão da CLI do Rustyll + +Este documento apresenta um plano detalhado para aprimorar a interface de linha de comando (CLI) do Rustyll. As propostas a seguir foram pensadas sob a perspectiva de um engenheiro multipapel – contemplando design de UX/CLI, arquitetura de software, desenvolvimento e garantia de qualidade. + +## 1. Análise de Usabilidade e Consistência + +### 1.1 Revisão de Nomenclatura +- **Padronização dos comandos**: priorizar nomes completos (`build`, `serve`, etc.), mantendo _aliases_ curtos apenas quando realmente úteis e sem conflito. +- **Opções coerentes**: garantir que opções semelhantes mantenham a mesma forma em todos os comandos (ex.: `--verbose` e `--quiet`), evitando sinônimos desnecessários. + +### 1.2 Estrutura de Comandos e Subcomandos +- Avaliar a criação de subcomandos agrupadores, como `rustyll config` (subcomandos `get`, `set`, `list`) e `rustyll cache` (`status`, `clear`). +- Organizar opções globais (ex.: `--config`, `--source`) no nível superior da CLI, válidas para qualquer comando. + +### 1.3 Feedback ao Usuário +- Mensagens padronizadas de sucesso e erro, utilizando cores e ícones simples para melhor leitura. +- Exibir progresso em operações demoradas (barra ou _spinner_). Em builds longos, mostrar etapas concluídas. + +### 1.4 Discoverability +- Tornar `rustyll help` mais completo, trazendo exemplos de uso. +- Gerar scripts de _autocomplete_ para Bash, Zsh e Fish. + +### 1.5 Convenções Comuns +- Manter atalhos amplamente reconhecidos como `-h/--help`, `-V/--version`, `--dry-run` e `--force`. + +## 2. Refatoração e Design de Opções + +### 2.1 Consolidação de Opções +- Agrupar opções de paralelismo em um bloco único (`--parallel [markdown|sass|all]`, `--threads N`). +- Introduzir perfis de performance (`--mode dev|prod|ultra-fast`) que ajustam múltiplos parâmetros de maneira pré-definida. + +### 2.2 Opções Globais vs. Locais +- Definir claramente quais opções afetam todos os comandos (`--source`, `--destination`, `--config`, `--verbose`, `--quiet`, `--trace`). +- Manter específicas de cada comando apenas as realmente necessárias, evitando duplicação desnecessária. + +### 2.3 Configuração via CLI e Arquivo +- Estabelecer ordem de precedência: **CLI > _config.yml > padrão**. +- Documentar no help como as opções da CLI podem sobrescrever o arquivo de configuração. + +### 2.4 Remodelagem das Opções de Performance +- Criar subcomandos ou _flags_ dedicados para cache (`rustyll cache clear`, `rustyll build --cache [markdown|sass|liquid]`). +- Melhorar a opção de _benchmark/profile_, permitindo `build --profile --output report.json`. + +## 3. Expansão para Novas Funcionalidades + +### 3.1 Migradores +- Novo comando: `rustyll migrate `. + - Opções: `--force-overwrite`, `--keep-original-assets`, `--dry-run`, `--report-file`. + - Subcomando auxiliar: `rustyll migrate list-platforms`. + +### 3.2 Gerenciamento de Temas +- Extensão do `new-theme` para `rustyll theme` com subcomandos: + - `install ` + - `list` + - `apply ` + +### 3.3 Suporte a Plugins +- Prever `rustyll plugin` com `install`, `list`, `enable` e `disable`. +- Permitir repositórios de plugins em configuração. + +### 3.4 Aprimoramentos do `doctor` +- Validar configurações e sugerir correções automáticas ou links para documentação. + +## 4. Garantia de Qualidade e Testabilidade + +### 4.1 Cobertura de Testes para CLI +- Implementar testes automatizados que executem comandos em ambiente isolado, verificando combinações de opções e saídas esperadas. +- Utilizar crates como `assert_cmd` e `escargot` para orquestrar execucoes de binarios durante os testes. + +### 4.2 Testes de Regressão +- Garantir paridade com comportamentos já existentes e com o Jekyll sempre que aplicável. +- Manter suite de testes que previna quebra de compatibilidade. + +### 4.3 Testes de Borda e Robustez +- Casos com caminhos inexistentes, permissões insuficientes e entradas inválidas. +- Uso de `--dry-run` para simular operações sem alterações reais. + +### 4.4 Saídas Consistentes +- Mensagens de erro e sucesso padronizadas em todas as ferramentas. +- Estrutura de logs configurável (níveis: error, warn, info, debug, trace). + +## 5. Documentação e Exemplos + +### 5.1 Estrutura da Documentação +- Criar seção dedicada à CLI no manual do projeto, listando cada comando, opções, exemplos e notas de compatibilidade Jekyll. + +### 5.2 Receitas (Cookbooks) +- Exemplos prontos demonstrando combinações de opções, como "Build de produção otimizado" ou "Servidor de desenvolvimento com live reload". + +### 5.3 Fluxos de Trabalho Típicos +- Descrever passo a passo desde a criação de um novo site até a publicação, usando os novos comandos. + +### 5.4 Atualização do TODO.md +- Registrar as tarefas propostas para acompanhamento da equipe. + +## Conclusão + +O plano acima visa tornar o Rustyll mais acessível, previsível e poderoso para usuários de todos os níveis, mantendo a compatibilidade com o ecossistema Jekyll sempre que benéfico. A refatoração estruturada da CLI, aliada a novas funcionalidades e ampla cobertura de testes, garantirá uma experiência robusta e moderna para a comunidade. + diff --git a/design/TODO.md b/design/TODO.md new file mode 100644 index 0000000..c20803e --- /dev/null +++ b/design/TODO.md @@ -0,0 +1,14 @@ +# TODO + +- [ ] Padronizar nomes de comandos e opções, mantendo aliases apenas quando necessários. +- [x] Introduzir subcomandos `config` e `cache` para organizar funcionalidades. +- [ ] Padronizar mensagens e feedback visual (sucesso, erro, progresso). +- [x] Implementar scripts de autocompletar para Bash, Zsh e Fish. +- [ ] Consolidar opções de paralelismo em `--parallel` e criar perfis `--mode`. +- [ ] Definir claramente opções globais e locais, documentando precedência CLI > _config.yml > padrão. +- [x] Implementar novos comandos `migrate`, `theme` e `plugin` conforme plano. +- [ ] Tornar o `doctor` mais proativo com sugestões de correção. +- [ ] Desenvolver suíte de testes de CLI abrangente, incluindo casos de borda. +- [ ] Implementar testes automatizados usando `assert_cmd` e `escargot`. +- [ ] Criar seção de documentação detalhada da CLI com exemplos e receitas. + diff --git a/src/cli/commands/cache.rs b/src/cli/commands/cache.rs new file mode 100644 index 0000000..0851fa0 --- /dev/null +++ b/src/cli/commands/cache.rs @@ -0,0 +1,58 @@ +use crate::cli::types::{Commands, CacheAction}; +use crate::directory::utils::clean_directory; +use std::path::PathBuf; + +pub async fn handle_cache_command(command: &Commands) { + if let Commands::Cache { action } = command { + match action { + CacheAction::Clear { kind } => { + clear_caches(kind.as_deref()); + } + CacheAction::Status {} => { + show_cache_status(); + } + } + } +} + +const CACHE_DIRS: &[&str] = &[".jekyll-cache", ".sass-cache"]; +const CACHE_FILES: &[&str] = &[".jekyll-metadata"]; + +fn clear_caches(kind: Option<&str>) { + match kind { + Some("sass") => remove_path(".sass-cache"), + Some("jekyll") => remove_path(".jekyll-cache"), + Some("metadata") => remove_path(".jekyll-metadata"), + Some(_) => println!("Unknown cache type"), + None => { + for d in CACHE_DIRS.iter().chain(CACHE_FILES.iter()) { + remove_path(d); + } + } + } +} + +fn show_cache_status() { + for d in CACHE_DIRS.iter().chain(CACHE_FILES.iter()) { + let path = PathBuf::from(d); + if path.exists() { + println!("{}: present", d); + } else { + println!("{}: not found", d); + } + } +} + +fn remove_path(path: &str) { + let p = PathBuf::from(path); + if p.is_dir() { + if let Err(e) = clean_directory(&p) { + println!("Failed to clean {}: {}", path, e); + } + } else if p.exists() { + if let Err(e) = std::fs::remove_file(&p) { + println!("Failed to remove {}: {}", path, e); + } + } +} + diff --git a/src/cli/commands/completions.rs b/src/cli/commands/completions.rs new file mode 100644 index 0000000..b99dc85 --- /dev/null +++ b/src/cli/commands/completions.rs @@ -0,0 +1,10 @@ +use clap_complete::{generate, Shell}; +use clap::{CommandFactory}; +use crate::cli::types::{Commands, Cli}; + +pub async fn handle_completions_command(command: &Commands) { + if let Commands::Completions { shell } = command { + let mut cmd = Cli::command(); + generate(*shell, &mut cmd, "rustyll", &mut std::io::stdout()); + } +} diff --git a/src/cli/commands/config.rs b/src/cli/commands/config.rs new file mode 100644 index 0000000..f9fd951 --- /dev/null +++ b/src/cli/commands/config.rs @@ -0,0 +1,51 @@ +use crate::cli::types::{Commands, ConfigAction}; +use crate::config; +use serde_yaml::Value; +use std::path::PathBuf; + +pub async fn handle_config_command(command: &Commands) { + if let Commands::Config { action } = command { + match action { + ConfigAction::Get { key } => { + if let Ok(cfg) = config::load_config(PathBuf::from("."), None) { + let yaml = serde_yaml::to_value(&cfg).unwrap_or(Value::Null); + if let Some(v) = get_nested_value(&yaml, key) { + println!("{}", serde_yaml::to_string(v).unwrap_or_default()); + } else { + println!("Key not found: {}", key); + } + } else { + println!("Failed to load configuration"); + } + } + ConfigAction::Set { key, value } => { + println!("Config set: {} = {}", key, value); + // TODO implement writing back to config file + } + ConfigAction::List {} => { + match config::load_config(PathBuf::from("."), None) { + Ok(cfg) => { + let yaml = serde_yaml::to_string(&cfg).unwrap_or_default(); + println!("{}", yaml); + } + Err(_) => println!("Failed to load configuration"), + } + } + } + } +} + +fn get_nested_value<'a>(value: &'a Value, key: &str) -> Option<&'a Value> { + let mut current = value; + for part in key.split('.') { + match current { + Value::Mapping(map) => { + let part_key = Value::String(part.to_string()); + current = map.get(&part_key)?; + } + _ => return None, + } + } + Some(current) +} + diff --git a/src/cli/commands/migrate.rs b/src/cli/commands/migrate.rs index d118961..1d1fd8e 100644 --- a/src/cli/commands/migrate.rs +++ b/src/cli/commands/migrate.rs @@ -1,5 +1,5 @@ use std::path::PathBuf; -use crate::cli::types::Commands; +use crate::cli::types::{Commands, MigrateCommands}; use crate::migrate; pub async fn handle_migrate_command( @@ -7,7 +7,7 @@ pub async fn handle_migrate_command( source_dir: Option<&PathBuf>, destination_dir: Option<&PathBuf> ) { - if let Commands::Migrate { source, destination, engine, verbose, clean } = command { + if let Commands::Migrate(MigrateCommands::Run { source, destination, engine, verbose, clean }) = command { // Determine source and destination directories let source_dir = if let Some(s) = source { s.clone() @@ -86,5 +86,11 @@ pub async fn handle_migrate_command( } } } + } else if let Commands::Migrate(MigrateCommands::ListPlatforms {}) = command { + let engines = migrate::list_engines(); + println!("Available engines:"); + for e in engines { + println!("- {}", e); + } } -} \ No newline at end of file +} diff --git a/src/cli/commands/mod.rs b/src/cli/commands/mod.rs index 150bbdc..6a6ca24 100644 --- a/src/cli/commands/mod.rs +++ b/src/cli/commands/mod.rs @@ -4,10 +4,21 @@ mod clean; mod report; mod migrate; mod new; +mod config; +mod cache; +mod theme; +mod plugin; +mod completions; pub use build::handle_build_command; pub use serve::handle_serve_command; pub use clean::handle_clean_command; pub use report::handle_report_command; pub use migrate::handle_migrate_command; -pub use new::handle_new_command; \ No newline at end of file +pub use new::handle_new_command; +pub use config::handle_config_command; +pub use cache::handle_cache_command; +pub use theme::handle_theme_command; +pub use plugin::handle_plugin_command; +pub use completions::handle_completions_command; + diff --git a/src/cli/commands/plugin.rs b/src/cli/commands/plugin.rs new file mode 100644 index 0000000..8c8d592 --- /dev/null +++ b/src/cli/commands/plugin.rs @@ -0,0 +1,18 @@ +use crate::cli::types::{Commands, PluginAction}; + +pub async fn handle_plugin_command(command: &Commands) { + if let Commands::Plugin { action } = command { + match action { + PluginAction::Install { name } => { + println!("Plugin install {}", name); + } + PluginAction::List {} => { + println!("Plugin list"); + } + PluginAction::Enable { name } => { + println!("Plugin enable {}", name); + } + } + } +} + diff --git a/src/cli/commands/theme.rs b/src/cli/commands/theme.rs new file mode 100644 index 0000000..44e6778 --- /dev/null +++ b/src/cli/commands/theme.rs @@ -0,0 +1,18 @@ +use crate::cli::types::{Commands, ThemeAction}; + +pub async fn handle_theme_command(command: &Commands) { + if let Commands::Theme { action } = command { + match action { + ThemeAction::Install { name_or_url } => { + println!("Theme install {}", name_or_url); + } + ThemeAction::List {} => { + println!("Theme list"); + } + ThemeAction::Apply { name } => { + println!("Theme apply {}", name); + } + } + } +} + diff --git a/src/cli/logging.rs b/src/cli/logging.rs index dc7ff9f..ca1ac68 100644 --- a/src/cli/logging.rs +++ b/src/cli/logging.rs @@ -30,4 +30,4 @@ pub fn configure_backtrace(trace: bool) { if trace { std::env::set_var("RUST_BACKTRACE", "1"); } -} \ No newline at end of file +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 46de249..47e3fe3 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -11,14 +11,11 @@ pub async fn run() { let cli = types::Cli::parse(); // Initialize logging system - logging::init_logging(cli.debug); + logging::init_logging(cli.debug || cli.verbose); // Configure backtrace logging::configure_backtrace(cli.trace); - // Set default source and destination - let source = cli.source.as_ref().map_or_else(|| PathBuf::from("./"), |p| p.clone()); - let destination = cli.destination.as_ref().map_or_else(|| PathBuf::from("./_site"), |p| p.clone()); match &cli.command { Some(types::Commands::Build { .. }) => { @@ -48,13 +45,28 @@ pub async fn run() { cli.source.as_ref() ).await; }, - Some(types::Commands::Migrate { .. }) => { + Some(types::Commands::Migrate(_)) => { commands::handle_migrate_command( &cli.command.as_ref().unwrap(), cli.source.as_ref(), cli.destination.as_ref() ).await; }, + Some(types::Commands::Config { .. }) => { + commands::handle_config_command(&cli.command.as_ref().unwrap()).await; + }, + Some(types::Commands::Cache { .. }) => { + commands::handle_cache_command(&cli.command.as_ref().unwrap()).await; + }, + Some(types::Commands::Theme { .. }) => { + commands::handle_theme_command(&cli.command.as_ref().unwrap()).await; + }, + Some(types::Commands::Plugin { .. }) => { + commands::handle_plugin_command(&cli.command.as_ref().unwrap()).await; + }, + Some(types::Commands::Completions { .. }) => { + commands::handle_completions_command(&cli.command.as_ref().unwrap()).await; + }, Some(types::Commands::New { .. }) => { commands::handle_new_command( &cli.command.as_ref().unwrap() @@ -77,4 +89,4 @@ pub async fn run() { } } } -} \ No newline at end of file +} diff --git a/src/cli/types.rs b/src/cli/types.rs index f1135e9..b16a515 100644 --- a/src/cli/types.rs +++ b/src/cli/types.rs @@ -1,4 +1,5 @@ use clap::{Parser, Subcommand}; +use clap_complete::Shell; use std::path::PathBuf; /// Main CLI parser structure @@ -37,6 +38,10 @@ pub struct Cli { /// Enable verbose debugging #[arg(short = 'g', long, default_value_t = false)] pub debug: bool, + + /// Print verbose output + #[arg(short, long, default_value_t = false)] + pub verbose: bool, } /// Subcommands for the CLI @@ -169,29 +174,6 @@ pub enum Commands { output: PathBuf, }, - /// Migrate a site from another static site generator to Rustyll - #[command(alias = "m")] - Migrate { - /// Source directory containing the site to be migrated - #[arg(short, long, value_name = "DIR")] - source: Option, - - /// Destination directory for the migrated site - #[arg(short, long, value_name = "DIR")] - destination: Option, - - /// Source engine to migrate from (e.g., jekyll, hugo, etc.) - #[arg(short = 'e', long, value_name = "ENGINE")] - engine: String, - - /// Print verbose output during migration - #[arg(short = 'v', long, default_value_t = false)] - verbose: bool, - - /// Clean the destination directory before migration - #[arg(short = 'c', long, default_value_t = false)] - clean: bool, - }, /// Creates a new Rustyll site scaffold in PATH #[command(alias = "n")] @@ -211,4 +193,107 @@ pub enum Commands { #[arg(long, default_value_t = false)] skip_bundle: bool, }, -} \ No newline at end of file + + /// Manage configuration values + Config { + #[command(subcommand)] + action: ConfigAction, + }, + + /// Manage build cache + Cache { + #[command(subcommand)] + action: CacheAction, + }, + + /// Manage themes + Theme { + #[command(subcommand)] + action: ThemeAction, + }, + + /// Manage plugins + Plugin { + #[command(subcommand)] + action: PluginAction, + }, + + /// Generate shell completion scripts + Completions { + /// Shell type (bash, zsh, fish, powershell, elvish) + #[arg(value_enum)] + shell: Shell, + }, + + /// Migration utilities + #[command(name = "migrate", alias = "m", subcommand)] + Migrate(MigrateCommands), +} + +#[derive(Subcommand)] +pub enum MigrateCommands { + /// Perform a migration from another static site generator + Run { + /// Source directory containing the site to be migrated + #[arg(short, long, value_name = "DIR")] + source: Option, + + /// Destination directory for the migrated site + #[arg(short, long, value_name = "DIR")] + destination: Option, + + /// Source engine to migrate from (e.g., jekyll, hugo, etc.) + #[arg(short = 'e', long, value_name = "ENGINE")] + engine: String, + + /// Print verbose output during migration + #[arg(short = 'v', long, default_value_t = false)] + verbose: bool, + + /// Clean the destination directory before migration + #[arg(short = 'c', long, default_value_t = false)] + clean: bool, + }, + + /// List supported migration engines + ListPlatforms {}, +} + +#[derive(Subcommand)] +pub enum ConfigAction { + /// Get value for a key + Get { key: String }, + /// Set configuration key + Set { key: String, value: String }, + /// List all keys + List {}, +} + +#[derive(Subcommand)] +pub enum CacheAction { + /// Clear cache + Clear { kind: Option }, + /// Show cache status + Status {}, +} + +#[derive(Subcommand)] +pub enum ThemeAction { + /// Install a theme + Install { name_or_url: String }, + /// List installed themes + List {}, + /// Apply a theme + Apply { name: String }, +} + +#[derive(Subcommand)] +pub enum PluginAction { + /// Install a plugin + Install { name: String }, + /// List installed plugins + List {}, + /// Enable a plugin + Enable { name: String }, +} + diff --git a/src/migrate/mod.rs b/src/migrate/mod.rs index ed1e9a2..18f4626 100644 --- a/src/migrate/mod.rs +++ b/src/migrate/mod.rs @@ -229,4 +229,14 @@ pub fn generate_migration_report(result: &MigrationResult, dest_dir: &Path) -> R .map_err(|e| format!("Failed to write migration report: {}", e))?; Ok(report_path) -} \ No newline at end of file +} + +/// Return the list of supported engine names +pub fn list_engines() -> Vec<&'static str> { + vec![ + "jekyll", "hugo", "zola", "eleventy", "gatsby", "docsy", "mdbook", + "mkdocs", "gitbook", "slate", "pelican", "nanoc", "middleman", + "assemble", "bridgetown", "cobalt", "fresh", "harp", "jigsaw", + "metalsmith", "nikola", "octopress", + ] +}