1use crate::model::{OptionType, ProbeResult};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum Language {
6 Rust,
8 Python,
10 JavaScript,
12 TypeScript,
14}
15
16impl Language {
17 pub fn from_str(s: &str) -> Option<Self> {
35 match s.to_lowercase().as_str() {
36 "rust" => Some(Language::Rust),
37 "python" | "py" => Some(Language::Python),
38 "javascript" | "js" => Some(Language::JavaScript),
39 "typescript" | "ts" => Some(Language::TypeScript),
40 _ => None,
41 }
42 }
43}
44
45pub fn generate_command_builder(result: &ProbeResult, language: Language) -> String {
77 match language {
78 Language::Rust => generate_rust_builder(result),
79 Language::Python => generate_python_builder(result),
80 Language::JavaScript => generate_javascript_builder(result),
81 Language::TypeScript => generate_typescript_builder(result),
82 }
83}
84
85fn generate_rust_builder(result: &ProbeResult) -> String {
87 let cmd_name = sanitize_identifier(&result.command);
88 let builder_name = format!("{}Builder", capitalize_first(&cmd_name));
89
90 let mut code = String::new();
91 code.push_str(&format!(
92 "// Generated command builder for {}\n",
93 result.command
94 ));
95 code.push_str("// Generated by help-probe\n\n");
96
97 code.push_str("use std::process::Command;\n\n");
98
99 code.push_str(&format!("pub struct {} {{\n", builder_name));
100 code.push_str(" command: String,\n");
101 code.push_str(" args: Vec<String>,\n");
102 code.push_str("}\n\n");
103
104 code.push_str(&format!("impl {} {{\n", builder_name));
105 code.push_str(" pub fn new() -> Self {\n");
106 code.push_str(&format!(" Self {{\n"));
107 code.push_str(&format!(
108 " command: \"{}\".to_string(),\n",
109 result.command
110 ));
111 code.push_str(" args: Vec::new(),\n");
112 code.push_str(" }\n");
113 code.push_str(" }\n\n");
114
115 for opt in &result.options {
117 for long_flag in &opt.long_flags {
118 let method_name = sanitize_identifier(&long_flag.trim_start_matches("--"));
119
120 if opt.takes_argument {
121 let arg_type = match opt.option_type {
122 OptionType::Number => "i64",
123 OptionType::Path => "&str",
124 OptionType::String => "&str",
125 OptionType::Choice => "&str",
126 OptionType::Boolean => "&str",
127 };
128
129 code.push_str(&format!(
130 " pub fn {}(mut self, value: {}) -> Self {{\n",
131 method_name, arg_type
132 ));
133 code.push_str(&format!(
134 " self.args.push(\"{}\".to_string());\n",
135 long_flag
136 ));
137 code.push_str(&format!(" self.args.push(value.to_string());\n"));
138 code.push_str(" self\n");
139 code.push_str(" }\n\n");
140 } else {
141 code.push_str(&format!(
142 " pub fn {}(mut self) -> Self {{\n",
143 method_name
144 ));
145 code.push_str(&format!(
146 " self.args.push(\"{}\".to_string());\n",
147 long_flag
148 ));
149 code.push_str(" self\n");
150 code.push_str(" }\n\n");
151 }
152 }
153 }
154
155 for (idx, arg) in result.arguments.iter().enumerate() {
157 let method_name = if arg.variadic {
158 format!("arg{}", idx + 1)
159 } else {
160 sanitize_identifier(&arg.name.to_lowercase())
161 };
162
163 let arg_type = match arg.arg_type {
164 Some(crate::model::ArgumentType::Number) => "i64",
165 Some(crate::model::ArgumentType::Path) => "&str",
166 Some(crate::model::ArgumentType::Url) => "&str",
167 Some(crate::model::ArgumentType::Email) => "&str",
168 _ => "&str",
169 };
170
171 if arg.variadic {
172 code.push_str(&format!(
173 " pub fn {}(mut self, values: Vec<{}>) -> Self {{\n",
174 method_name, arg_type
175 ));
176 code.push_str(" for value in values {\n");
177 code.push_str(" self.args.push(value.to_string());\n");
178 code.push_str(" }\n");
179 code.push_str(" self\n");
180 code.push_str(" }\n\n");
181 } else {
182 code.push_str(&format!(
183 " pub fn {}(mut self, value: {}) -> Self {{\n",
184 method_name, arg_type
185 ));
186 code.push_str(&format!(" self.args.push(value.to_string());\n"));
187 code.push_str(" self\n");
188 code.push_str(" }\n\n");
189 }
190 }
191
192 for subcmd in &result.subcommands {
194 let method_name = sanitize_identifier(&subcmd.name);
195 code.push_str(&format!(
196 " pub fn {}(mut self) -> Self {{\n",
197 method_name
198 ));
199 code.push_str(&format!(
200 " self.args.push(\"{}\".to_string());\n",
201 subcmd.name
202 ));
203 code.push_str(" self\n");
204 code.push_str(" }\n\n");
205 }
206
207 code.push_str(" pub fn build(&self) -> Command {\n");
209 code.push_str(" let mut cmd = Command::new(&self.command);\n");
210 code.push_str(" cmd.args(&self.args);\n");
211 code.push_str(" cmd\n");
212 code.push_str(" }\n\n");
213
214 code.push_str(" pub fn execute(&self) -> std::io::Result<std::process::Output> {\n");
215 code.push_str(" self.build().output()\n");
216 code.push_str(" }\n");
217
218 code.push_str("}\n");
219
220 code
221}
222
223fn generate_python_builder(result: &ProbeResult) -> String {
225 let cmd_name = sanitize_identifier(&result.command);
226 let builder_name = format!("{}Builder", capitalize_first(&cmd_name));
227
228 let mut code = String::new();
229 code.push_str(&format!(
230 "# Generated command builder for {}\n",
231 result.command
232 ));
233 code.push_str("# Generated by help-probe\n\n");
234
235 code.push_str("import subprocess\nfrom typing import List, Optional, Union\n\n");
236
237 code.push_str(&format!("class {}:\n", builder_name));
238 code.push_str(" def __init__(self):\n");
239 code.push_str(&format!(" self.command = \"{}\"\n", result.command));
240 code.push_str(" self.args: List[str] = []\n\n");
241
242 for opt in &result.options {
244 for long_flag in &opt.long_flags {
245 let method_name = sanitize_identifier(&long_flag.trim_start_matches("--"));
246
247 if opt.takes_argument {
248 code.push_str(&format!(
249 " def {}(self, value: str) -> '{}':\n",
250 method_name, builder_name
251 ));
252 code.push_str(&format!(" self.args.append(\"{}\")\n", long_flag));
253 code.push_str(" self.args.append(str(value))\n");
254 code.push_str(" return self\n\n");
255 } else {
256 code.push_str(&format!(
257 " def {}(self) -> '{}':\n",
258 method_name, builder_name
259 ));
260 code.push_str(&format!(" self.args.append(\"{}\")\n", long_flag));
261 code.push_str(" return self\n\n");
262 }
263 }
264 }
265
266 for (idx, arg) in result.arguments.iter().enumerate() {
268 let method_name = if arg.variadic {
269 format!("arg{}", idx + 1)
270 } else {
271 sanitize_identifier(&arg.name.to_lowercase())
272 };
273
274 if arg.variadic {
275 code.push_str(&format!(
276 " def {}(self, values: List[str]) -> '{}':\n",
277 method_name, builder_name
278 ));
279 code.push_str(" for value in values:\n");
280 code.push_str(" self.args.append(str(value))\n");
281 code.push_str(" return self\n\n");
282 } else {
283 code.push_str(&format!(
284 " def {}(self, value: str) -> '{}':\n",
285 method_name, builder_name
286 ));
287 code.push_str(" self.args.append(str(value))\n");
288 code.push_str(" return self\n\n");
289 }
290 }
291
292 for subcmd in &result.subcommands {
294 let method_name = sanitize_identifier(&subcmd.name);
295 code.push_str(&format!(
296 " def {}(self) -> '{}':\n",
297 method_name, builder_name
298 ));
299 code.push_str(&format!(" self.args.append(\"{}\")\n", subcmd.name));
300 code.push_str(" return self\n\n");
301 }
302
303 code.push_str(" def build(self) -> List[str]:\n");
305 code.push_str(" return [self.command] + self.args\n\n");
306
307 code.push_str(" def execute(self) -> subprocess.CompletedProcess:\n");
308 code.push_str(" return subprocess.run(self.build(), capture_output=True, text=True)\n");
309
310 code
311}
312
313fn generate_javascript_builder(result: &ProbeResult) -> String {
315 let cmd_name = sanitize_identifier(&result.command);
316 let builder_name = format!("{}Builder", capitalize_first(&cmd_name));
317
318 let mut code = String::new();
319 code.push_str(&format!(
320 "// Generated command builder for {}\n",
321 result.command
322 ));
323 code.push_str("// Generated by help-probe\n\n");
324
325 code.push_str("const { spawn } = require('child_process');\n\n");
326
327 code.push_str(&format!("class {} {{\n", builder_name));
328 code.push_str(" constructor() {\n");
329 code.push_str(&format!(" this.command = \"{}\";\n", result.command));
330 code.push_str(" this.args = [];\n");
331 code.push_str(" }\n\n");
332
333 for opt in &result.options {
335 for long_flag in &opt.long_flags {
336 let method_name = sanitize_identifier(&long_flag.trim_start_matches("--"));
337
338 if opt.takes_argument {
339 code.push_str(&format!(" {}(value) {{\n", method_name));
340 code.push_str(&format!(" this.args.push(\"{}\");\n", long_flag));
341 code.push_str(" this.args.push(String(value));\n");
342 code.push_str(" return this;\n");
343 code.push_str(" }\n\n");
344 } else {
345 code.push_str(&format!(" {}() {{\n", method_name));
346 code.push_str(&format!(" this.args.push(\"{}\");\n", long_flag));
347 code.push_str(" return this;\n");
348 code.push_str(" }\n\n");
349 }
350 }
351 }
352
353 for (idx, arg) in result.arguments.iter().enumerate() {
355 let method_name = if arg.variadic {
356 format!("arg{}", idx + 1)
357 } else {
358 sanitize_identifier(&arg.name.to_lowercase())
359 };
360
361 if arg.variadic {
362 code.push_str(&format!(" {}(values) {{\n", method_name));
363 code.push_str(" for (const value of values) {\n");
364 code.push_str(" this.args.push(String(value));\n");
365 code.push_str(" }\n");
366 code.push_str(" return this;\n");
367 code.push_str(" }\n\n");
368 } else {
369 code.push_str(&format!(" {}(value) {{\n", method_name));
370 code.push_str(" this.args.push(String(value));\n");
371 code.push_str(" return this;\n");
372 code.push_str(" }\n\n");
373 }
374 }
375
376 for subcmd in &result.subcommands {
378 let method_name = sanitize_identifier(&subcmd.name);
379 code.push_str(&format!(" {}() {{\n", method_name));
380 code.push_str(&format!(" this.args.push(\"{}\");\n", subcmd.name));
381 code.push_str(" return this;\n");
382 code.push_str(" }\n\n");
383 }
384
385 code.push_str(" build() {\n");
387 code.push_str(" return [this.command, ...this.args];\n");
388 code.push_str(" }\n\n");
389
390 code.push_str(" execute() {\n");
391 code.push_str(" return new Promise((resolve, reject) => {\n");
392 code.push_str(" const proc = spawn(this.command, this.args);\n");
393 code.push_str(" let stdout = '';\n");
394 code.push_str(" let stderr = '';\n");
395 code.push_str(" proc.stdout.on('data', (data) => { stdout += data; });\n");
396 code.push_str(" proc.stderr.on('data', (data) => { stderr += data; });\n");
397 code.push_str(" proc.on('close', (code) => {\n");
398 code.push_str(" resolve({ code, stdout, stderr });\n");
399 code.push_str(" });\n");
400 code.push_str(" proc.on('error', reject);\n");
401 code.push_str(" });\n");
402 code.push_str(" }\n");
403
404 code.push_str("}\n\n");
405 code.push_str(&format!("module.exports = {};\n", builder_name));
406
407 code
408}
409
410fn generate_typescript_builder(result: &ProbeResult) -> String {
412 let cmd_name = sanitize_identifier(&result.command);
413 let builder_name = format!("{}Builder", capitalize_first(&cmd_name));
414
415 let mut code = String::new();
416 code.push_str(&format!(
417 "// Generated command builder for {}\n",
418 result.command
419 ));
420 code.push_str("// Generated by help-probe\n\n");
421
422 code.push_str("import { spawn } from 'child_process';\n\n");
423
424 code.push_str(&format!("export class {} {{\n", builder_name));
425 code.push_str(" private command: string;\n");
426 code.push_str(" private args: string[];\n\n");
427
428 code.push_str(" constructor() {\n");
429 code.push_str(&format!(" this.command = \"{}\";\n", result.command));
430 code.push_str(" this.args = [];\n");
431 code.push_str(" }\n\n");
432
433 for opt in &result.options {
435 for long_flag in &opt.long_flags {
436 let method_name = sanitize_identifier(&long_flag.trim_start_matches("--"));
437
438 if opt.takes_argument {
439 code.push_str(&format!(
440 " {}(value: string): {} {{\n",
441 method_name, builder_name
442 ));
443 code.push_str(&format!(" this.args.push(\"{}\");\n", long_flag));
444 code.push_str(" this.args.push(value);\n");
445 code.push_str(" return this;\n");
446 code.push_str(" }\n\n");
447 } else {
448 code.push_str(&format!(" {}(): {} {{\n", method_name, builder_name));
449 code.push_str(&format!(" this.args.push(\"{}\");\n", long_flag));
450 code.push_str(" return this;\n");
451 code.push_str(" }\n\n");
452 }
453 }
454 }
455
456 for (idx, arg) in result.arguments.iter().enumerate() {
458 let method_name = if arg.variadic {
459 format!("arg{}", idx + 1)
460 } else {
461 sanitize_identifier(&arg.name.to_lowercase())
462 };
463
464 if arg.variadic {
465 code.push_str(&format!(
466 " {}(values: string[]): {} {{\n",
467 method_name, builder_name
468 ));
469 code.push_str(" this.args.push(...values);\n");
470 code.push_str(" return this;\n");
471 code.push_str(" }\n\n");
472 } else {
473 code.push_str(&format!(
474 " {}(value: string): {} {{\n",
475 method_name, builder_name
476 ));
477 code.push_str(" this.args.push(value);\n");
478 code.push_str(" return this;\n");
479 code.push_str(" }\n\n");
480 }
481 }
482
483 for subcmd in &result.subcommands {
485 let method_name = sanitize_identifier(&subcmd.name);
486 code.push_str(&format!(" {}(): {} {{\n", method_name, builder_name));
487 code.push_str(&format!(" this.args.push(\"{}\");\n", subcmd.name));
488 code.push_str(" return this;\n");
489 code.push_str(" }\n\n");
490 }
491
492 code.push_str(" build(): string[] {\n");
494 code.push_str(" return [this.command, ...this.args];\n");
495 code.push_str(" }\n\n");
496
497 code.push_str(
498 " execute(): Promise<{ code: number | null; stdout: string; stderr: string }> {\n",
499 );
500 code.push_str(" return new Promise((resolve, reject) => {\n");
501 code.push_str(" const proc = spawn(this.command, this.args);\n");
502 code.push_str(" let stdout = '';\n");
503 code.push_str(" let stderr = '';\n");
504 code.push_str(
505 " proc.stdout?.on('data', (data) => { stdout += data.toString(); });\n",
506 );
507 code.push_str(
508 " proc.stderr?.on('data', (data) => { stderr += data.toString(); });\n",
509 );
510 code.push_str(" proc.on('close', (code) => {\n");
511 code.push_str(" resolve({ code, stdout, stderr });\n");
512 code.push_str(" });\n");
513 code.push_str(" proc.on('error', reject);\n");
514 code.push_str(" });\n");
515 code.push_str(" }\n");
516
517 code.push_str("}\n");
518
519 code
520}
521
522fn sanitize_identifier(s: &str) -> String {
524 s.chars()
525 .map(|c| {
526 if c.is_alphanumeric() || c == '_' {
527 c
528 } else {
529 '_'
530 }
531 })
532 .collect::<String>()
533 .trim_start_matches('_')
534 .to_string()
535}
536
537fn capitalize_first(s: &str) -> String {
539 let mut chars = s.chars();
540 match chars.next() {
541 None => String::new(),
542 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
543 }
544}