Skip to main content

ubt_cli/commands/
init.rs

1use crate::detect::detect_tool;
2use crate::error::UbtError;
3use crate::plugin::{PluginRegistry, ResolvedPlugin};
4
5pub fn cmd_init() -> Result<(), UbtError> {
6    let cwd = std::env::current_dir()?;
7    let config_path = cwd.join("ubt.toml");
8
9    if config_path.exists() {
10        println!("ubt.toml already exists at {}", config_path.display());
11        return Ok(());
12    }
13
14    let registry = PluginRegistry::new()?;
15    let (tool, example_cmd) = match detect_tool(None, None, &cwd, &registry) {
16        Ok(detection) => {
17            let example = registry
18                .get(&detection.plugin_name)
19                .and_then(|(plugin, source)| {
20                    plugin
21                        .resolve_variant(&detection.variant_name, source.clone())
22                        .ok()
23                })
24                .and_then(|resolved| init_example_command(&resolved))
25                .unwrap_or_else(|| r#"start = "your-command-here""#.to_string());
26            (detection.variant_name, example)
27        }
28        Err(_) => ("npm".to_string(), r#"start = "npm run dev""#.to_string()),
29    };
30
31    let content = format!(
32        r#"# ubt.toml — Universal Build Tool configuration
33
34[project]
35# Pin the tool/runtime. Remove this line to let ubt auto-detect.
36# Supported: npm, pnpm, yarn, bun, deno, cargo, go, pip, uv, poetry, bundler
37tool = "{tool}"
38
39# Override built-in commands with project-specific shell commands.
40# Available keys: build, start, test, lint, fmt, check, clean, run, exec,
41#   dep.install, dep.remove, dep.update, dep.list, dep.audit, dep.outdated,
42#   db.migrate, db.rollback, db.seed, db.create, db.drop, db.reset, db.status
43# Use {{{{args}}}} to forward extra CLI arguments to the underlying command.
44[commands]
45# {example_cmd}
46# ...
47
48# Add new commands not covered by built-ins.
49# Names must not conflict with built-ins (build, test, dep, db, …).
50[aliases]
51# hello = "echo hello world"
52"#,
53        tool = tool,
54        example_cmd = example_cmd
55    );
56
57    std::fs::write(&config_path, &content)?;
58    println!("Created {}", config_path.display());
59    Ok(())
60}
61
62fn init_example_command(resolved: &ResolvedPlugin) -> Option<String> {
63    let preferred = ["start", "build", "test"];
64    for key in &preferred {
65        if let Some(cmd) = resolved.commands.get(*key) {
66            let rendered = cmd.replace("{{tool}}", &resolved.binary);
67            return Some(format!(r#"{key} = "{rendered}""#));
68        }
69    }
70    // fallback: first command alphabetically
71    let mut keys: Vec<&String> = resolved.commands.keys().collect();
72    keys.sort();
73    keys.first().map(|key| {
74        let rendered = resolved.commands[*key].replace("{{tool}}", &resolved.binary);
75        format!(r#"{key} = "{rendered}""#)
76    })
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use crate::plugin::{PluginSource, ResolvedPlugin};
83    use std::collections::HashMap;
84
85    fn make_resolved(binary: &str, commands: &[(&str, &str)]) -> ResolvedPlugin {
86        let cmds = commands
87            .iter()
88            .map(|(k, v)| (k.to_string(), v.to_string()))
89            .collect();
90        ResolvedPlugin {
91            name: "test".to_string(),
92            description: String::new(),
93            homepage: None,
94            install_help: None,
95            variant_name: "default".to_string(),
96            binary: binary.to_string(),
97            commands: cmds,
98            flags: HashMap::new(),
99            unsupported: HashMap::new(),
100            source: PluginSource::BuiltIn,
101        }
102    }
103
104    #[test]
105    fn init_example_prefers_start() {
106        let resolved = make_resolved(
107            "node",
108            &[
109                ("start", "{{tool}} run dev"),
110                ("build", "{{tool}} run build"),
111                ("test", "{{tool}} test"),
112            ],
113        );
114        let result = init_example_command(&resolved).unwrap();
115        assert!(result.starts_with("start = "), "got: {result}");
116        assert!(result.contains("node run dev"));
117        assert!(!result.contains("{{tool}}"));
118    }
119
120    #[test]
121    fn init_example_falls_back_to_build() {
122        let resolved = make_resolved(
123            "go",
124            &[
125                ("build", "{{tool}} build ./..."),
126                ("test", "{{tool}} test ./..."),
127            ],
128        );
129        let result = init_example_command(&resolved).unwrap();
130        assert!(result.starts_with("build = "), "got: {result}");
131        assert!(result.contains("go build ./..."));
132    }
133
134    #[test]
135    fn init_example_falls_back_to_test() {
136        let resolved = make_resolved(
137            "custom",
138            &[("test", "{{tool}} test"), ("check", "{{tool}} check")],
139        );
140        let result = init_example_command(&resolved).unwrap();
141        assert!(result.starts_with("test = "), "got: {result}");
142    }
143
144    #[test]
145    fn init_example_falls_back_alphabetically() {
146        // No start/build/test — should pick first alphabetically: "clean" < "fmt"
147        let resolved = make_resolved(
148            "cargo",
149            &[("fmt", "{{tool}} fmt"), ("clean", "{{tool}} clean")],
150        );
151        let result = init_example_command(&resolved).unwrap();
152        assert!(result.starts_with("clean = "), "got: {result}");
153    }
154
155    #[test]
156    fn init_example_returns_none_for_empty_commands() {
157        let resolved = make_resolved("mytool", &[]);
158        assert!(init_example_command(&resolved).is_none());
159    }
160
161    #[test]
162    fn init_example_replaces_tool_placeholder() {
163        let resolved = make_resolved("cargo", &[("test", "{{tool}} test --all")]);
164        let result = init_example_command(&resolved).unwrap();
165        assert!(
166            !result.contains("{{tool}}"),
167            "placeholder not replaced: {result}"
168        );
169        assert!(result.contains("cargo test --all"));
170    }
171}