diff --git a/.markdownlintignore b/.markdownlintignore index a08320bd7..15454185b 100644 --- a/.markdownlintignore +++ b/.markdownlintignore @@ -3,3 +3,4 @@ target/ CHANGELOG.md docs/node_modules/ node_modules/ +test/ diff --git a/Cargo.lock b/Cargo.lock index de4118010..8d58204f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4106,9 +4106,9 @@ checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" [[package]] name = "usage-lib" -version = "0.7.4" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef8f6a44b06866b37810400ecc3ee80dd0354448c870a60d424d6ab364c7e50b" +checksum = "4d2f461138354b775e96629ce474dad0f7b9996dd902db85665de06b8a2aaeda" dependencies = [ "clap", "heck 0.5.0", @@ -4120,6 +4120,7 @@ dependencies = [ "once_cell", "serde", "strum", + "tera", "thiserror", "xx", ] diff --git a/Cargo.toml b/Cargo.toml index fd6a0b2b2..292000dca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -116,7 +116,7 @@ toml = { version = "0.8", features = ["parse"] } toml_edit = { version = "0.22", features = ["parse"] } url = "2.5.0" #usage-lib = { path = "../usage/lib" } -usage-lib = { version = "0.7.0", features = ["clap"] } +usage-lib = { version = "0.8.0", features = ["clap", "docs"] } versions = { version = "6.2.0", features = ["serde"] } vfox = "0.1" walkdir = "2.5.0" diff --git a/docs/.vitepress/cli_commands.ts b/docs/.vitepress/cli_commands.ts index 2156705d3..d8a3b8662 100644 --- a/docs/.vitepress/cli_commands.ts +++ b/docs/.vitepress/cli_commands.ts @@ -111,6 +111,9 @@ export const commands: { [key: string]: Command } = { "github-action": { hide: false, }, + "task-docs": { + hide: false, + }, }, }, "global": { diff --git a/docs/cli/generate/task-docs.md b/docs/cli/generate/task-docs.md new file mode 100644 index 000000000..7c41afda0 --- /dev/null +++ b/docs/cli/generate/task-docs.md @@ -0,0 +1,28 @@ +## `mise generate task-docs [OPTIONS]` + +```text +[experimental] Generate documentation for tasks in a project + +Usage: generate task-docs [OPTIONS] + +Options: + -m, --multi + render each task as a separate document, requires `--output` to be a directory + + -i, --inject + inserts the documentation into an existing file + + This will look for a special comment, , and replace it with the generated documentation. + It will replace everything between the comment and the next comment, so it can be + run multiple times on the same file to update the documentation. + + -I, --index + write only an index of tasks, intended for use with `--multi` + + -o, --output + writes the generated docs to a file/directory + +Examples: + + $ mise generate task-docs +``` diff --git a/docs/cli/index.md b/docs/cli/index.md index 58d7ad498..d0e8afeb9 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -550,6 +550,35 @@ Examples: $ git push # runs `mise run ci` on GitHub ``` +## `mise generate task-docs [OPTIONS]` + +```text +[experimental] Generate documentation for tasks in a project + +Usage: generate task-docs [OPTIONS] + +Options: + -m, --multi + render each task as a separate document, requires `--output` to be a directory + + -i, --inject + inserts the documentation into an existing file + + This will look for a special comment, , and replace it with the generated documentation. + It will replace everything between the comment and the next comment, so it can be + run multiple times on the same file to update the documentation. + + -I, --index + write only an index of tasks, intended for use with `--multi` + + -o, --output + writes the generated docs to a file/directory + +Examples: + + $ mise generate task-docs +``` + ## `mise implode [OPTIONS]` ```text diff --git a/docs/tasks/running-tasks.md b/docs/tasks/running-tasks.md index 358f5b7c1..7881158e8 100644 --- a/docs/tasks/running-tasks.md +++ b/docs/tasks/running-tasks.md @@ -28,8 +28,12 @@ If there are multiple commands, the args are only passed to the last command. :::tip You can define arguments/flags for tasks which will provide validation, parsing, autocomplete, and documentation. -* [Arguments in File Tasks](/tasks/file-tasks.html#arguments) -* [Arguments in TOML Tasks](/tasks/toml-tasks.html#arguments) +* [Arguments in File Tasks](/tasks/file-tasks#arguments) +* [Arguments in TOML Tasks](/tasks/toml-tasks#arguments) + +Autocomplete will work automatically for tasks if the `usage` CLI is installed and mise completions are working. + +Markdown documentation can be generated with [`mise generate task-docs`](/cli/generate/task-docs). ::: Multiple tasks/arguments can be separated with this `:::` delimiter: diff --git a/mise.usage.kdl b/mise.usage.kdl index 3a233cd7d..ce8f6bf4b 100644 --- a/mise.usage.kdl +++ b/mise.usage.kdl @@ -348,7 +348,7 @@ The "--" separates runtimes from the commands to pass along to the subprocess."# arg "[COMMAND]..." help="Command string to execute (same as --command)" var=true } cmd "generate" subcommand_required=true help="[experimental] Generate files for various tools/services" { - alias "gen" + alias "g" cmd "git-pre-commit" help="[experimental] Generate a git pre-commit hook" { alias "pre-commit" long_help r"[experimental] Generate a git pre-commit hook @@ -387,9 +387,22 @@ when you push changes to your repository." } flag "-w --write" help="write to .github/workflows/$name.yml" } + cmd "task-docs" help="[experimental] Generate documentation for tasks in a project" { + after_long_help r"Examples: + + $ mise generate task-docs +" + flag "-m --multi" help="render each task as a separate document, requires `--output` to be a directory" + flag "-i --inject" help="inserts the documentation into an existing file" { + long_help "inserts the documentation into an existing file\n\nThis will look for a special comment, , and replace it with the generated documentation.\nIt will replace everything between the comment and the next comment, so it can be\nrun multiple times on the same file to update the documentation." + } + flag "-I --index" help="write only an index of tasks, intended for use with `--multi`" + flag "-o --output" help="writes the generated docs to a file/directory" { + arg "" + } + } } cmd "global" hide=true help="Sets/gets the global tool version(s)" { - alias "g" hide=true long_help r"Sets/gets the global tool version(s) Displays the contents of global config after writing. diff --git a/src/cli/generate/mod.rs b/src/cli/generate/mod.rs index 92b0af04d..89bcd53ec 100644 --- a/src/cli/generate/mod.rs +++ b/src/cli/generate/mod.rs @@ -2,10 +2,11 @@ use clap::Subcommand; mod git_pre_commit; mod github_action; +mod task_docs; /// [experimental] Generate files for various tools/services #[derive(Debug, clap::Args)] -#[clap(visible_alias = "gen")] +#[clap(visible_alias = "g")] pub struct Generate { #[clap(subcommand)] command: Commands, @@ -15,6 +16,7 @@ pub struct Generate { enum Commands { GitPreCommit(git_pre_commit::GitPreCommit), GithubAction(github_action::GithubAction), + TaskDocs(task_docs::TaskDocs), } impl Commands { @@ -22,6 +24,7 @@ impl Commands { match self { Self::GitPreCommit(cmd) => cmd.run(), Self::GithubAction(cmd) => cmd.run(), + Self::TaskDocs(cmd) => cmd.run(), } } } diff --git a/src/cli/generate/snapshots/mise__cli__generate__task_docs__tests__task_docs.snap b/src/cli/generate/snapshots/mise__cli__generate__task_docs__tests__task_docs.snap new file mode 100644 index 000000000..7c3d9c27c --- /dev/null +++ b/src/cli/generate/snapshots/mise__cli__generate__task_docs__tests__task_docs.snap @@ -0,0 +1,9 @@ +--- +source: src/cli/generate/task_docs.rs +expression: output +--- +# `filetask` + +## Flag `--user ` + +The user to run as diff --git a/src/cli/generate/task_docs.rs b/src/cli/generate/task_docs.rs new file mode 100644 index 000000000..14bc45d65 --- /dev/null +++ b/src/cli/generate/task_docs.rs @@ -0,0 +1,95 @@ +use crate::config::settings::SETTINGS; +use crate::config::CONFIG; +use crate::{dirs, file}; +use std::path::PathBuf; + +/// [experimental] Generate documentation for tasks in a project +#[derive(Debug, clap::Args)] +#[clap(verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)] +pub struct TaskDocs { + /// render each task as a separate document, requires `--output` to be a directory + #[clap(long, short, verbatim_doc_comment)] + multi: bool, + /// inserts the documentation into an existing file + /// + /// This will look for a special comment, , and replace it with the generated documentation. + /// It will replace everything between the comment and the next comment, so it can be + /// run multiple times on the same file to update the documentation. + #[clap(long, short, verbatim_doc_comment)] + inject: bool, + /// write only an index of tasks, intended for use with `--multi` + #[clap(long, short = 'I', verbatim_doc_comment)] + index: bool, + /// writes the generated docs to a file/directory + #[clap(long, short, verbatim_doc_comment)] + output: Option, +} + +impl TaskDocs { + pub fn run(self) -> eyre::Result<()> { + SETTINGS.ensure_experimental("generate task-docs")?; + let tasks = CONFIG.load_tasks_in_dir(dirs::CWD.as_ref().unwrap())?; + let mut out = vec![]; + for task in &tasks { + out.push(task.render_markdown()?); + } + if let Some(output) = &self.output { + if self.multi { + if output.is_dir() { + for (i, task) in tasks.iter().enumerate() { + let path = output.join(format!("task-{}.md", i)); + file::write(&path, &task.render_markdown()?)?; + } + } else { + return Err(eyre::eyre!( + "`--output` must be a directory when `--multi` is set" + )); + } + } else { + let mut doc = String::new(); + for task in out { + doc.push_str(&task); + doc.push_str("\n\n"); + } + if self.inject { + let mut contents = file::read_to_string(output)?; + let start = contents.find("").unwrap_or(0); + let end = contents[start..] + .find("") + .unwrap_or(contents.len()); + contents.replace_range(start..end, &doc); + file::write(output, &contents)?; + } else { + file::write(output, &doc)?; + } + } + } else { + for task in out { + miseprintln!("{}", task); + } + } + Ok(()) + } +} + +static AFTER_LONG_HELP: &str = color_print::cstr!( + r#"Examples: + + $ mise generate task-docs +"# +); + +#[cfg(test)] +mod tests { + use test_log::test; + + use crate::test::{cleanup, reset, setup_git_repo}; + + #[test] + fn test_task_docs() { + reset(); + setup_git_repo(); + assert_cli_snapshot!("generate", "task-docs"); + cleanup(); + } +} diff --git a/src/cli/global.rs b/src/cli/global.rs index b749253c3..77ac427a3 100644 --- a/src/cli/global.rs +++ b/src/cli/global.rs @@ -15,7 +15,7 @@ use crate::config::Settings; /// /// Use `mise local` to set a tool version locally in the current directory. #[derive(Debug, clap::Args)] -#[clap(verbatim_doc_comment, hide = true, alias = "g", after_long_help = AFTER_LONG_HELP)] +#[clap(verbatim_doc_comment, hide = true, after_long_help = AFTER_LONG_HELP)] pub struct Global { /// Tool(s) to add to .tool-versions /// e.g.: node@20 diff --git a/src/config/mod.rs b/src/config/mod.rs index a767134d6..ce2c19f6d 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -252,7 +252,7 @@ impl Config { .collect::>() } - fn load_tasks_in_dir(&self, dir: &Path) -> Result> { + pub fn load_tasks_in_dir(&self, dir: &Path) -> Result> { let configs = self.configs_at_root(dir); let config_tasks = configs .par_iter() diff --git a/src/task/mod.rs b/src/task/mod.rs index d5d2d22c8..eb3b36be5 100644 --- a/src/task/mod.rs +++ b/src/task/mod.rs @@ -220,6 +220,11 @@ impl Task { .collect()) } } + + pub fn render_markdown(&self) -> Result { + let (spec, _) = self.parse_usage_spec(None)?; + Ok(spec.render_markdown()?) + } } fn name_from_path(root: impl AsRef, path: impl AsRef) -> Result {