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