diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 76c90ed3..e4a7b2f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -189,6 +189,10 @@ jobs: go install github.com/google/yamlfmt/cmd/yamlfmt@latest go install golang.org/x/tools/cmd/goimports@latest echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" + # shuck ships prebuilt binaries (the linter is the shuck-cli package; + # `shuck check -` needs >= 0.0.43 for stdin support). + curl -fsSL https://github.com/ewhauser/shuck/releases/latest/download/shuck-cli-x86_64-unknown-linux-gnu.tar.xz | tar -xJ -C /tmp + sudo install /tmp/shuck-cli-x86_64-unknown-linux-gnu/shuck /usr/local/bin/ - name: Run code-block tools execution harness run: make test-code-block-tools diff --git a/docs/code-block-tools.md b/docs/code-block-tools.md index 13707e18..6035cd42 100644 --- a/docs/code-block-tools.md +++ b/docs/code-block-tools.md @@ -107,40 +107,41 @@ rumdl includes definitions for common tools: -| Tool ID | Language | Type | Command | -| ----------------- | ---------- | ------ | -------------------------------------- | -| `ruff:check` | Python | Lint | `ruff check --output-format=concise -` | -| `ruff:format` | Python | Format | `ruff format -` | -| `black` | Python | Format | `black --quiet -` | -| `prettier` | Multi | Format | `prettier --stdin-filepath=_.EXT` | -| `shellcheck` | Shell | Lint | `shellcheck --shell=bash -` | -| `shfmt` | Shell | Format | `shfmt` | -| `rustfmt` | Rust | Format | `rustfmt` | -| `gofmt` | Go | Format | `gofmt` | -| `goimports` | Go | Format | `goimports` | -| `clang-format` | C/C++ | Format | `clang-format` | -| `sqlfluff:lint` | SQL | Lint | `sqlfluff lint --dialect ansi -` | -| `sqlfluff:fix` | SQL | Format | `sqlfluff fix --dialect ansi -` | -| `jq` | JSON | Both | `jq .` | -| `yamlfmt` | YAML | Format | `yamlfmt -` | -| `taplo` | TOML | Format | `taplo fmt -` | -| `terraform-fmt` | Terraform | Format | `terraform fmt -` | -| `nixfmt` | Nix | Format | `nixfmt -` | -| `stylua` | Lua | Format | `stylua -` | -| `ormolu` | Haskell | Format | `ormolu --stdin-input-file=_.hs` | -| `elm-format` | Elm | Format | `elm-format --stdin` | -| `swift-format` | Swift | Format | `swift-format format -` | -| `ktfmt` | Kotlin | Format | `ktfmt -` | -| `djlint` | Jinja/HTML | Both | `djlint - / djlint - --reformat` | -| `djlint:lint` | Jinja/HTML | Lint | `djlint -` | -| `djlint:reformat` | Jinja/HTML | Format | `djlint - --reformat` | -| `beautysh` | Shell | Both | `beautysh - --check / beautysh -` | -| `tombi` | TOML | Lint | `tombi lint -` | -| `tombi:format` | TOML | Format | `tombi format -` | -| `tombi:lint` | TOML | Lint | `tombi lint -` | -| `oxfmt` | Multi | Format | `oxfmt --stdin-filepath=_.EXT` | -| `deno-fmt` | Multi | Format | `deno fmt --ext=EXT -` | -| `rumdl` | Markdown | Lint | `built-in markdown linting` | +| Tool ID | Language | Type | Command | +| ----------------- | ---------- | ------ | --------------------------------------- | +| `ruff:check` | Python | Lint | `ruff check --output-format=concise -` | +| `ruff:format` | Python | Format | `ruff format -` | +| `black` | Python | Format | `black --quiet -` | +| `prettier` | Multi | Format | `prettier --stdin-filepath=_.EXT` | +| `shellcheck` | Shell | Lint | `shellcheck --shell=bash -` | +| `shfmt` | Shell | Format | `shfmt` | +| `shuck` | Shell | Lint | `shuck check --output-format concise -` | +| `rustfmt` | Rust | Format | `rustfmt` | +| `gofmt` | Go | Format | `gofmt` | +| `goimports` | Go | Format | `goimports` | +| `clang-format` | C/C++ | Format | `clang-format` | +| `sqlfluff:lint` | SQL | Lint | `sqlfluff lint --dialect ansi -` | +| `sqlfluff:fix` | SQL | Format | `sqlfluff fix --dialect ansi -` | +| `jq` | JSON | Both | `jq .` | +| `yamlfmt` | YAML | Format | `yamlfmt -` | +| `taplo` | TOML | Format | `taplo fmt -` | +| `terraform-fmt` | Terraform | Format | `terraform fmt -` | +| `nixfmt` | Nix | Format | `nixfmt -` | +| `stylua` | Lua | Format | `stylua -` | +| `ormolu` | Haskell | Format | `ormolu --stdin-input-file=_.hs` | +| `elm-format` | Elm | Format | `elm-format --stdin` | +| `swift-format` | Swift | Format | `swift-format format -` | +| `ktfmt` | Kotlin | Format | `ktfmt -` | +| `djlint` | Jinja/HTML | Both | `djlint - / djlint - --reformat` | +| `djlint:lint` | Jinja/HTML | Lint | `djlint -` | +| `djlint:reformat` | Jinja/HTML | Format | `djlint - --reformat` | +| `beautysh` | Shell | Both | `beautysh - --check / beautysh -` | +| `tombi` | TOML | Lint | `tombi lint -` | +| `tombi:format` | TOML | Format | `tombi format -` | +| `tombi:lint` | TOML | Lint | `tombi lint -` | +| `oxfmt` | Multi | Format | `oxfmt --stdin-filepath=_.EXT` | +| `deno-fmt` | Multi | Format | `deno fmt --ext=EXT -` | +| `rumdl` | Markdown | Lint | `built-in markdown linting` | @@ -373,7 +374,7 @@ zsh = "shell" | Feature | rumdl | mdsf | | ---------------- | -------------- | ---------- | -| Built-in tools | 32 | 339 | +| Built-in tools | 33 | 339 | | Custom tools | Yes | Yes | | Linting | Yes | No | | Formatting | Yes | Yes | diff --git a/src/code_block_tools/registry.rs b/src/code_block_tools/registry.rs index f9909c10..89a81f40 100644 --- a/src/code_block_tools/registry.rs +++ b/src/code_block_tools/registry.rs @@ -210,6 +210,28 @@ static BUILTIN_TOOLS: LazyLock> = LazyLock }, ); + // Shell - shuck (lint only; faster shellcheck alternative). `--output-format + // concise` keeps shuck's legacy one-line-per-diagnostic output, which parses + // via the same generic "file:line:col: message" path as other tools instead + // of needing a dedicated parser. Requires shuck >= 0.0.43 for `check -` stdin + // support (see rvben/rumdl#655 and ewhauser/shuck#1123). + m.insert( + "shuck", + ToolDefinition { + command: vec![ + "shuck".to_string(), + "check".to_string(), + "--output-format".to_string(), + "concise".to_string(), + "-".to_string(), + ], + stdin: true, + stdout: true, + lint_args: vec![], + format_args: vec![], + }, + ); + // Rust - rustfmt m.insert( "rustfmt", @@ -729,6 +751,14 @@ const BUILTIN_TOOLS_DOCS: &[ToolDocMeta] = &[ display_command: None, runtime: true, }, + ToolDocMeta { + id: "shuck", + language: "Shell", + kind: ToolKind::Lint, + doc_group: "shuck", + display_command: None, + runtime: true, + }, ToolDocMeta { id: "rustfmt", language: "Rust", diff --git a/tests/integration/code_block_tools_execution_test.rs b/tests/integration/code_block_tools_execution_test.rs index 8a5448c3..cb3adeb5 100644 --- a/tests/integration/code_block_tools_execution_test.rs +++ b/tests/integration/code_block_tools_execution_test.rs @@ -57,6 +57,16 @@ fn tool_available(tool: &str) -> bool { .map(|o| o.status.success()) .unwrap_or(false) } + "shuck" => { + // `shuck` is a contested binary name: the shell linter ships as the + // `shuck-cli` package, while an unrelated microVM manager also + // installs a `shuck`. Only the linter has a `check` subcommand. + Command::new("shuck") + .args(["check", "--help"]) + .output() + .map(|o| o.status.success()) + .unwrap_or(false) + } _ => { // Default spawn check (safe, won't block even if tool expects stdin, // because we pass --version and kill it immediately if it spawns) @@ -147,6 +157,20 @@ fn shellcheck_lints_shell() { ); } +#[test] +fn shuck_lints_shell() { + require_tool!("shuck"); + // Regression guard for the stdin fix upstream (ewhauser/shuck#1123, shipped in + // v0.0.43): a pre-0.0.43 shuck treats `-` as a literal filename instead of + // reading stdin and would report a missing-file error here instead of a + // real diagnostic. + let out = lint("shell", "shuck", "shell", "name=\"world\"\necho \"hello $nombre\"\n"); + assert!( + out.contains("referenced before assignment") || out.contains("C006"), + "shuck should flag the reference to the undefined variable:\n{out}" + ); +} + #[test] fn jq_lints_invalid_json() { require_tool!("jq"); @@ -367,6 +391,7 @@ const VERIFIED: &[&str] = &[ "prettier", "shellcheck", "shfmt", + "shuck", "rustfmt", "gofmt", "goimports",