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, ®istry) {
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 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 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}