1- #![ cfg_attr(
2- clippy,
3- allow(
4- clippy:: cast_possible_truncation,
5- clippy:: cast_possible_wrap,
6- clippy:: cast_sign_loss,
7- // things are often more readable this way
8- clippy:: needless_raw_string_hashes,
9- clippy:: cast_lossless,
10- clippy:: module_name_repetitions,
11- clippy:: type_complexity,
12- clippy:: zero_prefixed_literal,
13- clippy:: unused_self,
14- clippy:: if_same_then_else,
15- clippy:: needless_continue,
16- // correctly used
17- clippy:: enum_glob_use,
18- clippy:: result_unit_err,
19- // not practical
20- clippy:: similar_names,
21- clippy:: too_many_lines,
22- clippy:: struct_excessive_bools,
23- // preference
24- clippy:: doc_markdown,
25- clippy:: unnecessary_wraps,
26- clippy:: ref_as_ptr,
27- // false positive
28- clippy:: needless_doctest_main,
29- // noisy
30- clippy:: missing_errors_doc,
31- clippy:: use_self,
32- clippy:: cognitive_complexity,
33- clippy:: option_if_let_else,
34- clippy:: implicit_clone,
35- ) ,
36- warn(
37- clippy:: missing_asserts_for_indexing,
38- )
39- ) ]
40-
41- // qsv-skill-gen: Generate Agent Skills from qsv command USAGE text
1+ // qsv MCP Skills Generator - Generate Agent Skills from qsv command USAGE text
422//
43- // This tool parses USAGE text from qsv commands and generates Agent Skill
44- // definitions in JSON format for use with the Claude Agent SDK.
3+ // This module parses USAGE text from qsv commands and generates Agent Skill
4+ // definitions in JSON format for use with Claude Desktop (MCP) and the Claude Agent SDK.
455//
466// Uses qsv-docopt Parser for robust USAGE text parsing.
477
@@ -53,6 +13,8 @@ use std::{
5313use qsv_docopt:: parse:: { Argument as DocoptArgument , Atom , Parser } ;
5414use serde:: { Deserialize , Serialize } ;
5515
16+ use crate :: CliResult ;
17+
5618#[ derive( Debug , Serialize , Deserialize ) ]
5719struct SkillDefinition {
5820 name : String ,
@@ -565,7 +527,9 @@ fn extract_usage_from_file(file_path: &Path) -> Result<String, String> {
565527 Ok ( after_start[ ..usage_end] . to_string ( ) )
566528}
567529
568- fn main ( ) -> Result < ( ) , Box < dyn std:: error:: Error > > {
530+ /// Public function to generate MCP skills JSON files
531+ /// Called via `qsv --update-mcp-skills` flag
532+ pub fn generate_mcp_skills ( ) -> CliResult < ( ) > {
569533 // Get all commands from src/cmd/*.rs (excluding mod.rs and duplicates)
570534 let commands = vec ! [
571535 "apply" ,
@@ -636,22 +600,35 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
636600 "validate" ,
637601 ] ;
638602
603+ // Determine repository root - look for Cargo.toml
604+ let mut repo_root = std:: env:: current_dir ( ) ?;
605+ loop {
606+ if repo_root. join ( "Cargo.toml" ) . exists ( ) && repo_root. join ( "src/cmd" ) . exists ( ) {
607+ break ;
608+ }
609+ if !repo_root. pop ( ) {
610+ return fail_clierror ! ( "Could not find qsv repository root (no Cargo.toml with src/cmd)" ) ;
611+ }
612+ }
613+
639614 // Create output directory
640- let output_dir = PathBuf :: from ( ".claude/skills/qsv" ) ;
615+ let output_dir = repo_root . join ( ".claude/skills/qsv" ) ;
641616 fs:: create_dir_all ( & output_dir) ?;
642617
643- println ! ( "QSV Agent Skill Generator (using qsv-docopt)" ) ;
644- println ! ( "=============================================" ) ;
645- println ! ( "Generating {} skills...\n " , commands. len( ) ) ;
618+ eprintln ! ( "QSV MCP Skills Generator (via qsv --update-mcp-skills)" ) ;
619+ eprintln ! ( "=======================================================" ) ;
620+ eprintln ! ( "Repository: {}" , repo_root. display( ) ) ;
621+ eprintln ! ( "Output: {}" , output_dir. display( ) ) ;
622+ eprintln ! ( "Generating {} skills...\n " , commands. len( ) ) ;
646623
647624 let mut success_count = 0 ;
648625 let mut error_count = 0 ;
649626
650627 for cmd_name in & commands {
651- println ! ( "Processing: {cmd_name}" ) ;
628+ eprintln ! ( "Processing: {cmd_name}" ) ;
652629
653630 // Find command file
654- let cmd_file = PathBuf :: from ( format ! ( "src/cmd/{cmd_name}.rs" ) ) ;
631+ let cmd_file = repo_root . join ( format ! ( "src/cmd/{cmd_name}.rs" ) ) ;
655632
656633 if !cmd_file. exists ( ) {
657634 eprintln ! ( " ❌ File not found: {}" , cmd_file. display( ) ) ;
@@ -685,22 +662,28 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
685662 let json = serde_json:: to_string_pretty ( & skill) ?;
686663 fs:: write ( & output_file, json) ?;
687664
688- println ! ( " ✅ Generated: {}" , output_file. display( ) ) ;
689- println ! ( " - {} arguments" , skill. command. args. len( ) ) ;
690- println ! ( " - {} options" , skill. command. options. len( ) ) ;
691- println ! ( ) ;
665+ eprintln ! ( " ✅ Generated: {}" , output_file. display( ) ) ;
666+ eprintln ! ( " - {} arguments" , skill. command. args. len( ) ) ;
667+ eprintln ! ( " - {} options" , skill. command. options. len( ) ) ;
668+ eprintln ! ( ) ;
692669
693670 success_count += 1 ;
694671 }
695672
696- println ! ( "\n ✨ Skill generation complete!" ) ;
697- println ! ( "📁 Output directory: {}" , output_dir. display( ) ) ;
698- println ! (
673+ eprintln ! ( "\n ✨ MCP Skills generation complete!" ) ;
674+ eprintln ! ( "📁 Output directory: {}" , output_dir. display( ) ) ;
675+ eprintln ! (
699676 "📊 Summary: {} succeeded, {} failed out of {} total" ,
700677 success_count,
701678 error_count,
702679 commands. len( )
703680 ) ;
704681
682+ if error_count > 0 {
683+ return fail_clierror ! ( "{} skill(s) failed to generate" , error_count) ;
684+ }
685+
686+ eprintln ! ( "\n 💡 Restart Claude Desktop to load the updated skills." ) ;
687+
705688 Ok ( ( ) )
706689}
0 commit comments