baobao_codegen_typescript/adapters/
boune.rs

1//! Boune CLI framework adapter for TypeScript/Bun.
2
3use baobao_codegen::{
4    adapters::{CliAdapter, CliInfo, CommandMeta, Dependency, DispatchInfo, ImportSpec},
5    builder::CodeFragment,
6};
7use baobao_core::ArgType;
8use baobao_manifest::ArgType as ManifestArgType;
9
10use crate::{
11    BOUNE_VERSION,
12    ast::{ArrowFn, JsObject},
13};
14
15/// Boune adapter for generating TypeScript CLI code targeting Bun runtime.
16#[derive(Debug, Clone, Default)]
17pub struct BouneAdapter;
18
19impl BouneAdapter {
20    pub fn new() -> Self {
21        Self
22    }
23
24    /// Convert manifest ArgType to core ArgType.
25    fn convert_arg_type(arg_type: &ManifestArgType) -> ArgType {
26        match arg_type {
27            ManifestArgType::String => ArgType::String,
28            ManifestArgType::Int => ArgType::Int,
29            ManifestArgType::Float => ArgType::Float,
30            ManifestArgType::Bool => ArgType::Bool,
31            ManifestArgType::Path => ArgType::Path,
32        }
33    }
34
35    /// Build an argument object schema for boune's declarative API using manifest types.
36    pub fn build_argument_schema_manifest(
37        &self,
38        arg_type: &ManifestArgType,
39        required: bool,
40        default: Option<&toml::Value>,
41        description: Option<&str>,
42        choices: Option<&[String]>,
43    ) -> String {
44        self.build_argument_schema(
45            Self::convert_arg_type(arg_type),
46            required,
47            default,
48            description,
49            choices,
50        )
51    }
52
53    /// Build an argument object schema for boune's declarative API.
54    ///
55    /// Generates: `{ type: "string", required: true, description: "...", choices: [...] as const }`
56    pub fn build_argument_schema(
57        &self,
58        arg_type: ArgType,
59        required: bool,
60        default: Option<&toml::Value>,
61        description: Option<&str>,
62        choices: Option<&[String]>,
63    ) -> String {
64        let boune_type = self.map_arg_type(arg_type);
65        let mut parts = vec![format!("type: \"{}\"", boune_type)];
66
67        if required && default.is_none() {
68            parts.push("required: true".to_string());
69        }
70
71        if let Some(default) = default {
72            parts.push(format!("default: {}", toml_to_ts_literal(default)));
73        }
74
75        if let Some(desc) = description {
76            parts.push(format!("description: \"{}\"", desc));
77        }
78
79        if let Some(choices) = choices {
80            let choices_array = choices
81                .iter()
82                .map(|c| format!("\"{}\"", c))
83                .collect::<Vec<_>>()
84                .join(", ");
85            parts.push(format!("choices: [{}] as const", choices_array));
86        }
87
88        format!("{{ {} }}", parts.join(", "))
89    }
90
91    /// Build an option object schema for boune's declarative API using manifest types.
92    pub fn build_option_schema_manifest(
93        &self,
94        flag_type: &ManifestArgType,
95        short: Option<char>,
96        default: Option<&toml::Value>,
97        description: Option<&str>,
98        choices: Option<&[String]>,
99    ) -> String {
100        self.build_option_schema(
101            Self::convert_arg_type(flag_type),
102            short,
103            default,
104            description,
105            choices,
106        )
107    }
108
109    /// Build an option object schema for boune's declarative API.
110    ///
111    /// Generates: `{ type: "string", short: "x", default: ..., description: "...", choices: [...] as const }`
112    pub fn build_option_schema(
113        &self,
114        flag_type: ArgType,
115        short: Option<char>,
116        default: Option<&toml::Value>,
117        description: Option<&str>,
118        choices: Option<&[String]>,
119    ) -> String {
120        let boune_type = self.map_arg_type(flag_type);
121        let mut parts = vec![format!("type: \"{}\"", boune_type)];
122
123        if let Some(short) = short {
124            parts.push(format!("short: \"{}\"", short));
125        }
126
127        if let Some(default) = default {
128            parts.push(format!("default: {}", toml_to_ts_literal(default)));
129        }
130
131        if let Some(desc) = description {
132            parts.push(format!("description: \"{}\"", desc));
133        }
134
135        if let Some(choices) = choices {
136            let choices_array = choices
137                .iter()
138                .map(|c| format!("\"{}\"", c))
139                .collect::<Vec<_>>()
140                .join(", ");
141            parts.push(format!("choices: [{}] as const", choices_array));
142        }
143
144        format!("{{ {} }}", parts.join(", "))
145    }
146
147    /// Map manifest argument type to TypeScript boune type.
148    pub fn map_manifest_arg_type(&self, arg_type: &ManifestArgType) -> &'static str {
149        self.map_arg_type(Self::convert_arg_type(arg_type))
150    }
151
152    /// Build action handler arrow function.
153    pub fn build_action_handler(&self, has_args: bool, has_options: bool) -> ArrowFn {
154        // Build destructuring pattern based on what's available
155        let params = match (has_args, has_options) {
156            (true, true) => "{ args, options }",
157            (true, false) => "{ args }",
158            (false, true) => "{ options }",
159            (false, false) => "{}",
160        };
161
162        // Build run() call based on what's available
163        let run_call = match (has_args, has_options) {
164            (true, true) => "await run(args, options);",
165            (true, false) => "await run(args);",
166            (false, true) => "await run(options);",
167            (false, false) => "await run();",
168        };
169
170        ArrowFn::new(params).async_().body_line(run_call)
171    }
172}
173
174impl CliAdapter for BouneAdapter {
175    fn name(&self) -> &'static str {
176        "boune"
177    }
178
179    fn dependencies(&self) -> Vec<Dependency> {
180        vec![Dependency::new("boune", BOUNE_VERSION)]
181    }
182
183    fn generate_cli(&self, info: &CliInfo) -> Vec<CodeFragment> {
184        // Build CLI schema object
185        let mut schema = JsObject::new()
186            .string("name", &info.name)
187            .string("version", info.version.to_string());
188
189        if let Some(desc) = &info.description {
190            schema = schema.string("description", desc);
191        }
192
193        // Build commands object
194        let mut commands_obj = JsObject::new();
195        for cmd in &info.commands {
196            commands_obj =
197                commands_obj.raw(&cmd.pascal_name, format!("{}Command", cmd.pascal_name));
198        }
199        schema = schema.object("commands", commands_obj);
200
201        // Generate the defineCli call
202        let code = format!("const app = defineCli({});", schema.build());
203        vec![CodeFragment::raw(code)]
204    }
205
206    fn generate_command(&self, info: &CommandMeta) -> Vec<CodeFragment> {
207        // This generates a leaf command definition
208        let action = self.build_action_handler(!info.args.is_empty(), !info.flags.is_empty());
209
210        let schema = JsObject::new()
211            .string("name", &info.name)
212            .string("description", &info.description)
213            .raw_if(!info.args.is_empty(), "arguments", "args")
214            .raw_if(!info.flags.is_empty(), "options", "options")
215            .arrow_fn("action", action);
216
217        let code = format!(
218            "export const {}Command = defineCommand({});",
219            info.pascal_name,
220            schema.build()
221        );
222
223        vec![CodeFragment::raw(code)]
224    }
225
226    fn generate_subcommands(&self, info: &CommandMeta) -> Vec<CodeFragment> {
227        // Build subcommands object
228        let mut subcommands = JsObject::new();
229        for sub in &info.subcommands {
230            subcommands = subcommands.raw(&sub.pascal_name, format!("{}Command", sub.pascal_name));
231        }
232
233        let schema = JsObject::new()
234            .string("name", &info.name)
235            .string("description", &info.description)
236            .object("subcommands", subcommands);
237
238        let code = format!(
239            "export const {}Command = defineCommand({});",
240            info.pascal_name,
241            schema.build()
242        );
243
244        vec![CodeFragment::raw(code)]
245    }
246
247    fn generate_dispatch(&self, _info: &DispatchInfo) -> Vec<CodeFragment> {
248        // Boune handles dispatch internally via subcommands object
249        // No explicit dispatch code needed
250        Vec::new()
251    }
252
253    fn imports(&self) -> Vec<ImportSpec> {
254        vec![ImportSpec::new("boune").symbol("defineCli")]
255    }
256
257    fn command_imports(&self, info: &CommandMeta) -> Vec<ImportSpec> {
258        let mut imports = vec![ImportSpec::new("boune").symbol("defineCommand")];
259
260        // No longer need to import `argument` or `option` builders
261        // Type inference helpers are still needed
262        if !info.args.is_empty() {
263            imports.push(ImportSpec::new("boune").symbol("InferArgs").type_only());
264        }
265
266        if !info.flags.is_empty() {
267            imports.push(ImportSpec::new("boune").symbol("InferOpts").type_only());
268        }
269
270        imports
271    }
272
273    fn map_arg_type(&self, arg_type: ArgType) -> &'static str {
274        match arg_type {
275            ArgType::String => "string",
276            ArgType::Int => "number",
277            ArgType::Float => "number",
278            ArgType::Bool => "boolean",
279            ArgType::Path => "string",
280        }
281    }
282
283    fn map_optional_type(&self, arg_type: ArgType) -> String {
284        format!("{} | undefined", self.map_arg_type(arg_type))
285    }
286}
287
288/// Convert a TOML value to a TypeScript literal string.
289/// Strings are quoted, numbers and booleans are raw.
290fn toml_to_ts_literal(value: &toml::Value) -> String {
291    match value {
292        toml::Value::String(s) => format!("\"{}\"", s),
293        toml::Value::Integer(i) => i.to_string(),
294        toml::Value::Float(f) => f.to_string(),
295        toml::Value::Boolean(b) => b.to_string(),
296        _ => String::new(),
297    }
298}