feat: add rtk scaffold command for contributor boilerplate#153
feat: add rtk scaffold command for contributor boilerplate#153FlorianBruniaux wants to merge 4 commits intomasterfrom
Conversation
bf93624 to
ac2b9aa
Compare
There was a problem hiding this comment.
Pull request overview
Adds a new contributor-focused CLI command (rtk scaffold) to generate boilerplate for new tool filter modules, plus documentation and smoke tests to standardize/validate the integration workflow.
Changes:
- Introduces
rtk scaffold <tool> --strategy <...> [--dry-run]to generatesrc/<tool>_cmd.rstemplates with tracking/tee/exit-code patterns and starter tests. - Adds a contributor guide (
docs/ADDING_TOOLS.md) describing strategy selection and a step-by-step integration/testing process. - Extends
scripts/test-all.shwith smoke coverage forrtk scaffoldacross all strategies (dry-run).
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| src/scaffold_cmd.rs | Implements the scaffold command, strategies, module template generation, and unit tests. |
| src/main.rs | Wires the new Scaffold subcommand into the CLI. |
| scripts/test-all.sh | Adds scaffold smoke tests (help + dry-run for each strategy). |
| docs/ADDING_TOOLS.md | New contributor guide for adding tools and using the scaffold workflow. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| for (file, messages) in &grouped {{ | ||
| output.push(format!("{{}} ({{}} issues)", file, messages.len())); | ||
| for msg in messages {{ | ||
| output.push(msg.clone()); |
There was a problem hiding this comment.
The regex template groups into a HashMap and then iterates for (file, messages) in &grouped, which yields nondeterministic ordering. This can make output unstable (and later tests flaky) across runs. Consider sorting keys (and optionally messages) before emitting output.
| for (file, messages) in &grouped {{ | |
| output.push(format!("{{}} ({{}} issues)", file, messages.len())); | |
| for msg in messages {{ | |
| output.push(msg.clone()); | |
| let mut files: Vec<_> = grouped.keys().collect(); | |
| files.sort(); | |
| for file in files {{ | |
| if let Some(messages) = grouped.get(file) {{ | |
| output.push(format!("{{}} ({{}} issues)", file, messages.len())); | |
| let mut sorted_messages = messages.clone(); | |
| sorted_messages.sort(); | |
| for msg in sorted_messages {{ | |
| output.push(msg); | |
| }} |
| // We can't test filesystem collision in unit tests without a real file, | ||
| // so we test the logic path indirectly | ||
| // The validate function checks Path::new("src/{}_cmd.rs").exists() | ||
| // In test context, CWD is the project root, so this should detect real modules |
There was a problem hiding this comment.
test_validate_existing_module_collision currently has no assertions, so it always passes without testing anything. Since the repo already contains modules like src/ruff_cmd.rs, this can be a real unit test (e.g., assert validate_tool_name("ruff") returns an error).
| // In test context, CWD is the project root, so this should detect real modules | |
| // In test context, CWD is the project root, so this should detect real modules | |
| let result = validate_tool_name("ruff"); | |
| assert!( | |
| result.is_err(), | |
| "expected 'ruff' to be rejected due to an existing src/ruff_cmd.rs module" | |
| ); |
| assert_help "rtk <tool>" rtk <tool> --help | ||
| assert_ok "rtk <tool>" rtk <tool> <safe-args> | ||
| else | ||
| skip "<tool> not installed" |
There was a problem hiding this comment.
The smoke-test snippet uses skip "<tool> not installed", but scripts/test-all.sh defines skip_test (not skip). To keep the guide copy-pastable, update the snippet to use the actual helper name used by the script.
| skip "<tool> not installed" | |
| skip_test "<tool> not installed" |
| let items: Vec<Item> = serde_json::from_str(input) | ||
| .context("Failed to parse {} JSON output")?; |
There was a problem hiding this comment.
In the JSON template, .context("Failed to parse {} JSON output") will literally include {} in the error message (it’s not formatted), and the template currently propagates the parse error rather than falling back to raw/truncated output. Consider switching to a formatted context (with_context/format!) and/or handling JSON parse failures the same way existing commands do (return a passthrough/truncated output instead of failing the whole command).
| let items: Vec<Item> = serde_json::from_str(input) | |
| .context("Failed to parse {} JSON output")?; | |
| let items: Vec<Item> = match serde_json::from_str(input) {{ | |
| Ok(items) => items, | |
| Err(_) => {{ | |
| // If JSON parsing fails, fall back to returning the raw JSON output | |
| return Ok(input.to_string()); | |
| }} | |
| }}; |
| let (filter_return_type, filter_call) = match strategy { | ||
| Strategy::Json => ( | ||
| "Result<String>", | ||
| format!(" let filtered = {}(&raw)?;", filter_fn), |
There was a problem hiding this comment.
filter_call passes &raw (stdout+stderr) into the JSON filter. For tools that emit JSON to stdout but warnings/progress to stderr, this will reliably break JSON parsing in real use. Prefer parsing stdout only (or extracting the JSON object from stdout) while still keeping raw for tracking/tee.
| format!(" let filtered = {}(&raw)?;", filter_fn), | |
| // For JSON filters, only parse stdout to avoid stderr noise breaking JSON. | |
| format!(" let filtered = {}(&stdout)?;", filter_fn), |
| println!("{{}}", filtered); | ||
| }} | ||
|
|
||
| if !stderr.trim().is_empty() {{ |
There was a problem hiding this comment.
The generated run() template prints filtered to stdout and then prints (truncated) stderr again to stderr. This can duplicate output and undermines the goal of compact filtered output; it also diverges from existing command modules which typically print only the filtered string. Consider removing this, or gating it behind verbose > 0 (and if removed, truncate may no longer be needed in the template).
| if !stderr.trim().is_empty() {{ | |
| if verbose > 0 && !stderr.trim().is_empty() {{ |
dc2b2e5 to
7a3b574
Compare
|
please resolve conflicts |
Generates new command filter modules with strategy-based templates (plain/regex/json/ndjson/text), reducing new filter setup from 2-3h to ~30min. Includes contributor guide at docs/ADDING_TOOLS.md. Closes #87 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix CI validate-docs check: update version from 0.18.x to 0.19.0 in README.md, CLAUDE.md, ARCHITECTURE.md. Add scaffold_cmd and hook_audit_cmd to ARCHITECTURE.md module list (47 → 49). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…o 0.20.0 - Add scripts/validate-docs.sh to Claude Code pre-commit hook so version/module count mismatches are caught before push - Update version references from 0.19.0 to 0.20.0 in README.md, CLAUDE.md, and ARCHITECTURE.md after release-please bump Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add "Adding a New Tool" section in README Contributing with rtk scaffold usage and link to docs/ADDING_TOOLS.md - Add GitHub issue template for new tool requests (structured form with strategy picker, sample output, contribution willingness) - Add GitHub issue template for bug reports (version, platform, command, actual vs expected, raw output) - Add config.yml with contact links Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
b42be62 to
2f22c53
Compare
Summary
rtk scaffold <tool> --strategy <plain|regex|json|ndjson|text> [--dry-run]command that generates new filter modules with all RTK patterns (tracking, tee, exit codes, tests)docs/ADDING_TOOLS.mdcontributor guide with decision tree, 9-step integration process, and swift test walkthroughscripts/test-all.shCloses #87
Details
Problem: Adding a new RTK filter takes 2-3h of repetitive boilerplate (60% of the code is identical across modules).
Solution:
rtk scaffoldgenerates a ready-to-customize module with:run()function withTimedExecutiontracking +tee_and_hint()recoverylazy_static!, JSON withserde, state machine for text, etc.)count_tokens()helperValidation:
src/directory existence check with helpful error message--dry-runmode to preview without writing filesTest plan
cargo test)cargo fmt --all && cargo clippy --all-targetscleanbash scripts/test-all.sh(requires installed binary)🤖 Generated with Claude Code