1+ use clap:: { Args , Parser , Subcommand } ;
2+ use plotnik_lib:: Query ;
3+ use std:: fs;
4+ use std:: io:: { self , Read } ;
5+ use std:: path:: PathBuf ;
6+
7+ #[ derive( Parser ) ]
8+ #[ command( name = "plotnik" , bin_name = "plotnik" ) ]
9+ #[ command( about = "Query language for tree-sitter AST with type inference" ) ]
10+ #[ command( after_help = r#"OUTPUT DEPENDENCIES:
11+ ┌─────────────────┬─────────────┬──────────────┐
12+ │ Output │ Needs Query │ Needs Source │
13+ ├─────────────────┼─────────────┼──────────────┤
14+ │ --query-cst │ ✓ │ │
15+ │ --query-refs │ ✓ │ │
16+ │ --query-types │ ✓ │ │
17+ │ --source-ast │ │ ✓ │
18+ │ --trace │ ✓ │ ✓ │
19+ │ --result │ ✓ │ ✓ │
20+ └─────────────────┴─────────────┴──────────────┘
21+
22+ EXAMPLES:
23+ # Parse and typecheck query only
24+ plotnik --query-text '(identifier) @id' --query-cst --query-types
25+
26+ # Dump tree-sitter AST of source file
27+ plotnik --source-file app.ts --source-ast
28+
29+ # Full pipeline: match query against source
30+ plotnik --query-file rules.pql --source-file app.ts --result
31+
32+ # Debug with trace
33+ plotnik --query-text '(function_declaration) @fn' \
34+ --source-text 'function foo() {}' --lang typescript --trace
35+
36+ # Show documentation
37+ plotnik docs reference"# ) ]
38+ struct Cli {
39+ #[ command( subcommand) ]
40+ command : Option < Command > ,
41+
42+ #[ command( flatten) ]
43+ query : QueryArgs ,
44+
45+ #[ command( flatten) ]
46+ source : SourceArgs ,
47+
48+ /// Language for source (required for --source-text, inferred from extension for --source-file)
49+ #[ arg( long, short = 'l' , value_name = "LANG" ) ]
50+ lang : Option < String > ,
51+
52+ #[ command( flatten) ]
53+ output : OutputArgs ,
54+ }
55+
56+ #[ derive( Subcommand ) ]
57+ enum Command {
58+ /// Print documentation
59+ Docs {
60+ /// Topic to display (e.g., "reference", "examples")
61+ topic : Option < String > ,
62+ } ,
63+ }
64+
65+ #[ derive( Args ) ]
66+ #[ group( id = "query_input" , multiple = false ) ]
67+ struct QueryArgs {
68+ /// Query as inline text
69+ #[ arg( long, value_name = "QUERY" ) ]
70+ query_text : Option < String > ,
71+
72+ /// Query from file (use "-" for stdin)
73+ #[ arg( long, value_name = "FILE" ) ]
74+ query_file : Option < PathBuf > ,
75+ }
76+
77+ #[ derive( Args ) ]
78+ #[ group( id = "source_input" , multiple = false ) ]
79+ struct SourceArgs {
80+ /// Source code as inline text
81+ #[ arg( long, value_name = "SOURCE" ) ]
82+ source_text : Option < String > ,
83+
84+ /// Source code from file (use "-" for stdin)
85+ #[ arg( long, value_name = "FILE" ) ]
86+ source_file : Option < PathBuf > ,
87+ }
88+
89+ #[ derive( Args ) ]
90+ struct OutputArgs {
91+ /// Show parsed query CST (concrete syntax tree)
92+ #[ arg( long) ]
93+ query_cst : bool ,
94+
95+ /// Show name resolution (definitions and references)
96+ #[ arg( long) ]
97+ query_refs : bool ,
98+
99+ /// Show inferred output types
100+ #[ arg( long) ]
101+ query_types : bool ,
102+
103+ /// Show tree-sitter AST of source
104+ #[ arg( long) ]
105+ source_ast : bool ,
106+
107+ /// Show execution trace
108+ #[ arg( long) ]
109+ trace : bool ,
110+
111+ /// Show match results
112+ #[ arg( long) ]
113+ result : bool ,
114+ }
115+
1116fn main ( ) {
2- println ! ( "Hello, world!" ) ;
117+ let cli = Cli :: parse ( ) ;
118+
119+ if let Some ( Command :: Docs { topic } ) = cli. command {
120+ print_help ( topic. as_deref ( ) ) ;
121+ return ;
122+ }
123+
124+ let has_query = cli. query . query_text . is_some ( ) || cli. query . query_file . is_some ( ) ;
125+ let has_source = cli. source . source_text . is_some ( ) || cli. source . source_file . is_some ( ) ;
126+
127+ // Validate output dependencies
128+ if ( cli. output . query_cst || cli. output . query_refs || cli. output . query_types ) && !has_query {
129+ eprintln ! ( "error: --query-cst, --query-refs, and --query-types require --query-text or --query-file" ) ;
130+ std:: process:: exit ( 1 ) ;
131+ }
132+
133+ if cli. output . source_ast && !has_source {
134+ eprintln ! ( "error: --source-ast requires --source-text or --source-file" ) ;
135+ std:: process:: exit ( 1 ) ;
136+ }
137+
138+ if cli. output . trace && !( has_query && has_source) {
139+ eprintln ! ( "error: --trace requires both query and source inputs" ) ;
140+ std:: process:: exit ( 1 ) ;
141+ }
142+
143+ if cli. output . result && !( has_query && has_source) {
144+ eprintln ! ( "error: --result requires both query and source inputs" ) ;
145+ std:: process:: exit ( 1 ) ;
146+ }
147+
148+ // If both inputs provided and no outputs selected, default to --result
149+ let show_result = cli. output . result
150+ || ( has_query
151+ && has_source
152+ && !cli. output . query_cst
153+ && !cli. output . query_refs
154+ && !cli. output . query_types
155+ && !cli. output . source_ast
156+ && !cli. output . trace ) ;
157+
158+ // Validate --lang requirement
159+ if cli. source . source_text . is_some ( ) && cli. lang . is_none ( ) {
160+ eprintln ! ( "error: --lang is required when using --source-text" ) ;
161+ std:: process:: exit ( 1 ) ;
162+ }
163+
164+ // Load query if needed
165+ let query_source = if has_query {
166+ Some ( load_query ( & cli. query ) )
167+ } else {
168+ None
169+ } ;
170+
171+ let query = query_source. as_ref ( ) . map ( |src| Query :: new ( src) ) ;
172+
173+ if cli. output . query_cst {
174+ println ! ( "=== QUERY CST ===" ) ;
175+ if let Some ( ref q) = query {
176+ print ! ( "{}" , q. format_cst( ) ) ;
177+ }
178+ }
179+
180+ if cli. output . query_refs {
181+ println ! ( "=== QUERY REFS ===" ) ;
182+ if let Some ( ref q) = query {
183+ print ! ( "{}" , q. format_refs( ) ) ;
184+ }
185+ }
186+
187+ if cli. output . query_types {
188+ println ! ( "=== QUERY TYPES ===" ) ;
189+ println ! ( "(not implemented)" ) ;
190+ println ! ( ) ;
191+ }
192+
193+ if cli. output . source_ast {
194+ println ! ( "=== SOURCE AST ===" ) ;
195+ println ! ( "(not implemented)" ) ;
196+ println ! ( ) ;
197+ }
198+
199+ if cli. output . trace {
200+ println ! ( "=== TRACE ===" ) ;
201+ println ! ( "(not implemented)" ) ;
202+ println ! ( ) ;
203+ }
204+
205+ if show_result {
206+ println ! ( "=== RESULT ===" ) ;
207+ println ! ( "(not implemented)" ) ;
208+ println ! ( ) ;
209+ }
210+
211+ // Print query errors at the end, grouped by stage
212+ if let Some ( ref q) = query {
213+ if !q. is_valid ( ) {
214+ eprint ! ( "{}" , q. render_errors_grouped( ) ) ;
215+ }
216+ }
217+ }
218+
219+ fn load_query ( args : & QueryArgs ) -> String {
220+ if let Some ( ref text) = args. query_text {
221+ return text. clone ( ) ;
222+ }
223+ if let Some ( ref path) = args. query_file {
224+ if path. as_os_str ( ) == "-" {
225+ let mut buf = String :: new ( ) ;
226+ io:: stdin ( ) . read_to_string ( & mut buf) . expect ( "failed to read stdin" ) ;
227+ return buf;
228+ }
229+ return fs:: read_to_string ( path) . expect ( "failed to read query file" ) ;
230+ }
231+ unreachable ! ( )
3232}
233+
234+
235+
236+ fn print_help ( topic : Option < & str > ) {
237+ match topic {
238+ None => {
239+ println ! ( "Available topics:" ) ;
240+ println ! ( " reference - Query language reference" ) ;
241+ println ! ( " examples - Example queries" ) ;
242+ println ! ( ) ;
243+ println ! ( "Usage: plotnik docs <topic>" ) ;
244+ }
245+ Some ( "reference" ) => {
246+ // TODO: include_str! the actual REFERENCE.md
247+ println ! ( "{}" , include_str!( "../../../docs/REFERENCE.md" ) ) ;
248+ }
249+ Some ( "examples" ) => {
250+ println ! ( "(examples not yet written)" ) ;
251+ }
252+ Some ( other) => {
253+ eprintln ! ( "Unknown help topic: {}" , other) ;
254+ eprintln ! ( "Run 'plotnik help' to see available topics" ) ;
255+ std:: process:: exit ( 1 ) ;
256+ }
257+ }
258+ }
0 commit comments