Skip to main content

alef_core/config/
setup_defaults.rs

1use super::extras::Language;
2use super::output::{SetupConfig, StringOrVec};
3use super::tools::{LangContext, require_tool};
4
5/// Return the default setup configuration for a language.
6///
7/// The `output_dir` is the package directory where scaffolded files live
8/// (e.g. `packages/python`). It is substituted into command templates.
9/// `ctx` provides the package manager selection.
10pub(crate) fn default_setup_config(lang: Language, output_dir: &str, ctx: &LangContext) -> SetupConfig {
11    match lang {
12        Language::Rust => {
13            let mut commands: Vec<String> = vec!["rustup update stable".to_string()];
14            commands.extend(
15                ctx.tools
16                    .rust_tools()
17                    .iter()
18                    .map(|t| format!("cargo install {t} --locked")),
19            );
20            commands.push("rustup component add rustfmt clippy".to_string());
21            SetupConfig {
22                precondition: Some(require_tool("cargo")),
23                before: None,
24                install: Some(StringOrVec::Multiple(commands)),
25                timeout_seconds: 600,
26            }
27        }
28        Language::Python => {
29            let pm = ctx.tools.python_pm();
30            let install_cmd = match pm {
31                "pip" => format!("cd {output_dir} && pip install -e ."),
32                "poetry" => format!("cd {output_dir} && poetry install"),
33                _ => format!("cd {output_dir} && uv sync"),
34            };
35            SetupConfig {
36                precondition: Some(require_tool(pm)),
37                before: None,
38                install: Some(StringOrVec::Single(install_cmd)),
39                timeout_seconds: 600,
40            }
41        }
42        Language::Node | Language::Wasm => {
43            let pm = ctx.tools.node_pm();
44            let install_cmd = match pm {
45                "npm" => format!("cd {output_dir} && npm install"),
46                "yarn" => format!("cd {output_dir} && yarn install"),
47                _ => format!("cd {output_dir} && pnpm install"),
48            };
49            SetupConfig {
50                precondition: Some(require_tool(pm)),
51                before: None,
52                install: Some(StringOrVec::Single(install_cmd)),
53                timeout_seconds: 600,
54            }
55        }
56        Language::Go => SetupConfig {
57            precondition: Some(require_tool("go")),
58            before: None,
59            install: Some(StringOrVec::Single(format!(
60                "cd {output_dir} && GOWORK=off go mod download"
61            ))),
62            timeout_seconds: 600,
63        },
64        Language::Ruby => SetupConfig {
65            precondition: Some(require_tool("bundle")),
66            before: None,
67            install: Some(StringOrVec::Single(format!("cd {output_dir} && bundle install"))),
68            timeout_seconds: 600,
69        },
70        Language::Php => SetupConfig {
71            precondition: Some(require_tool("composer")),
72            before: None,
73            install: Some(StringOrVec::Single(format!("cd {output_dir} && composer install"))),
74            timeout_seconds: 600,
75        },
76        Language::Java => SetupConfig {
77            precondition: Some(require_tool("mvn")),
78            before: None,
79            install: Some(StringOrVec::Single(format!(
80                "mvn -f {output_dir}/pom.xml dependency:resolve -q"
81            ))),
82            timeout_seconds: 600,
83        },
84        Language::Csharp => SetupConfig {
85            // Both `dotnet` AND a discoverable .sln/.csproj must exist under output_dir, or
86            // `dotnet restore` walks the entire repo (including target/ and node_modules/)
87            // looking for a project file and times out. Skip cleanly when no project is present.
88            precondition: Some(format!(
89                "command -v dotnet >/dev/null 2>&1 && [ -n \"$(find {output_dir} -maxdepth 3 \\( -name '*.sln' -o -name '*.csproj' \\) 2>/dev/null | head -1)\" ]"
90            )),
91            before: None,
92            // Resolve the first .sln/.csproj under output_dir (depth 3) — same approach as
93            // the C# upgrade default. Avoids the unbounded directory walk that caused the
94            // 600s timeout on CI.
95            install: Some(StringOrVec::Single(format!(
96                "dotnet restore $(find {output_dir} -maxdepth 3 \\( -name '*.sln' -o -name '*.csproj' \\) 2>/dev/null | head -1)"
97            ))),
98            timeout_seconds: 600,
99        },
100        Language::Elixir => SetupConfig {
101            precondition: Some(require_tool("mix")),
102            before: None,
103            install: Some(StringOrVec::Single(format!("cd {output_dir} && mix deps.get"))),
104            timeout_seconds: 600,
105        },
106        Language::R => SetupConfig {
107            precondition: Some(require_tool("Rscript")),
108            before: None,
109            install: Some(StringOrVec::Single(format!(
110                "cd {output_dir} && Rscript -e \"remotes::install_deps()\""
111            ))),
112            timeout_seconds: 600,
113        },
114        Language::Ffi => SetupConfig {
115            // FFI shares cargo with the parent Rust crate; there is no
116            // separate install step and therefore nothing to precondition.
117            precondition: None,
118            before: None,
119            install: None,
120            timeout_seconds: 600,
121        },
122        Language::C => SetupConfig {
123            precondition: None,
124            before: None,
125            install: None,
126            timeout_seconds: 600,
127        },
128        Language::Kotlin => SetupConfig {
129            precondition: Some(require_tool("gradle")),
130            before: None,
131            install: Some(StringOrVec::Single("gradle build --refresh-dependencies".to_string())),
132            timeout_seconds: 600,
133        },
134        Language::Swift => SetupConfig {
135            precondition: Some(require_tool("swift")),
136            before: None,
137            install: Some(StringOrVec::Single("swift package resolve".to_string())),
138            timeout_seconds: 600,
139        },
140        Language::Dart => SetupConfig {
141            precondition: Some(require_tool("dart")),
142            before: None,
143            install: Some(StringOrVec::Single("dart pub get".to_string())),
144            timeout_seconds: 600,
145        },
146        Language::Gleam => SetupConfig {
147            precondition: Some(require_tool("gleam")),
148            before: None,
149            install: Some(StringOrVec::Single("gleam deps download".to_string())),
150            timeout_seconds: 600,
151        },
152        Language::Zig => SetupConfig {
153            precondition: Some(require_tool("zig")),
154            before: None,
155            install: Some(StringOrVec::Single("zig build --fetch".to_string())),
156            timeout_seconds: 600,
157        },
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::super::tools::ToolsConfig;
164    use super::*;
165
166    fn all_languages() -> Vec<Language> {
167        vec![
168            Language::Python,
169            Language::Node,
170            Language::Wasm,
171            Language::Ruby,
172            Language::Php,
173            Language::Go,
174            Language::Java,
175            Language::Csharp,
176            Language::Elixir,
177            Language::R,
178            Language::Ffi,
179            Language::Rust,
180            Language::Kotlin,
181            Language::Swift,
182            Language::Dart,
183            Language::Gleam,
184            Language::Zig,
185        ]
186    }
187
188    fn cfg(lang: Language, dir: &str) -> SetupConfig {
189        let tools = ToolsConfig::default();
190        let ctx = LangContext::default(&tools);
191        default_setup_config(lang, dir, &ctx)
192    }
193
194    #[test]
195    fn ffi_has_no_install_command() {
196        let c = cfg(Language::Ffi, "packages/ffi");
197        assert!(c.install.is_none());
198    }
199
200    #[test]
201    fn non_ffi_languages_have_install_command() {
202        for lang in all_languages() {
203            if matches!(lang, Language::Ffi) {
204                continue;
205            }
206            let c = cfg(lang, "packages/test");
207            assert!(c.install.is_some(), "{lang} should have a default install command");
208        }
209    }
210
211    #[test]
212    fn non_ffi_languages_have_default_precondition() {
213        for lang in all_languages() {
214            if matches!(lang, Language::Ffi) {
215                continue;
216            }
217            let c = cfg(lang, "packages/test");
218            let pre = c
219                .precondition
220                .unwrap_or_else(|| panic!("{lang} should have a precondition"));
221            assert!(pre.starts_with("command -v "));
222        }
223    }
224
225    #[test]
226    fn rust_install_lists_full_tool_set() {
227        let c = cfg(Language::Rust, "packages/rust");
228        let install = c.install.unwrap();
229        let cmds = install.commands();
230        let joined = cmds.join(" || ");
231        assert!(joined.contains("rustup update stable"));
232        for tool in super::super::tools::DEFAULT_RUST_DEV_TOOLS {
233            assert!(
234                joined.contains(&format!("cargo install {tool} --locked")),
235                "Rust setup should install {tool}, got: {joined}"
236            );
237        }
238        assert!(joined.contains("rustup component add rustfmt clippy"));
239    }
240
241    #[test]
242    fn rust_install_respects_user_tool_list() {
243        let tools = ToolsConfig {
244            rust_dev_tools: Some(vec!["cargo-edit".to_string(), "cargo-foo".to_string()]),
245            ..Default::default()
246        };
247        let ctx = LangContext::default(&tools);
248        let c = default_setup_config(Language::Rust, "packages/rust", &ctx);
249        let cmds = c.install.unwrap().commands().join(" || ");
250        assert!(cmds.contains("cargo install cargo-edit --locked"));
251        assert!(cmds.contains("cargo install cargo-foo --locked"));
252        // Default tools that aren't in the user override should be absent.
253        assert!(!cmds.contains("cargo install cargo-deny"));
254    }
255
256    fn python_tools(pm: &str) -> ToolsConfig {
257        ToolsConfig {
258            python_package_manager: Some(pm.to_string()),
259            ..Default::default()
260        }
261    }
262
263    fn node_tools(pm: &str) -> ToolsConfig {
264        ToolsConfig {
265            node_package_manager: Some(pm.to_string()),
266            ..Default::default()
267        }
268    }
269
270    #[test]
271    fn python_setup_dispatches_on_package_manager() {
272        for (pm, expected_install, expected_pre) in [
273            ("uv", "uv sync", "command -v uv >/dev/null 2>&1"),
274            ("pip", "pip install -e", "command -v pip >/dev/null 2>&1"),
275            ("poetry", "poetry install", "command -v poetry >/dev/null 2>&1"),
276        ] {
277            let tools = python_tools(pm);
278            let ctx = LangContext::default(&tools);
279            let c = default_setup_config(Language::Python, "packages/python", &ctx);
280            assert!(c.install.unwrap().commands().join(" ").contains(expected_install));
281            assert_eq!(c.precondition.as_deref(), Some(expected_pre));
282        }
283    }
284
285    #[test]
286    fn node_setup_dispatches_on_package_manager() {
287        for (pm, expected_install) in [
288            ("pnpm", "pnpm install"),
289            ("npm", "npm install"),
290            ("yarn", "yarn install"),
291        ] {
292            let tools = node_tools(pm);
293            let ctx = LangContext::default(&tools);
294            let c = default_setup_config(Language::Node, "packages/node", &ctx);
295            assert!(c.install.unwrap().commands().join(" ").contains(expected_install));
296        }
297    }
298
299    #[test]
300    fn python_uses_uv_sync_by_default() {
301        let c = cfg(Language::Python, "packages/python");
302        let install = c.install.unwrap().commands().join(" ");
303        assert!(install.contains("uv sync"));
304        assert!(install.contains("packages/python"));
305    }
306
307    #[test]
308    fn node_uses_pnpm_install_by_default() {
309        let c = cfg(Language::Node, "packages/node");
310        let install = c.install.unwrap().commands().join(" ");
311        assert!(install.contains("pnpm install"));
312    }
313
314    #[test]
315    fn wasm_matches_node() {
316        // Same package manager invocation, only the output dir differs.
317        let node = cfg(Language::Node, "packages/foo");
318        let wasm = cfg(Language::Wasm, "packages/foo");
319        assert_eq!(
320            node.install.unwrap().commands().join(" "),
321            wasm.install.unwrap().commands().join(" "),
322            "WASM and Node should share install command"
323        );
324    }
325
326    #[test]
327    fn go_uses_go_mod_download() {
328        let c = cfg(Language::Go, "packages/go");
329        let install = c.install.unwrap().commands().join(" ");
330        assert!(install.contains("go mod download"));
331    }
332
333    #[test]
334    fn ruby_uses_bundle_install() {
335        let c = cfg(Language::Ruby, "packages/ruby");
336        let install = c.install.unwrap().commands().join(" ");
337        assert!(install.contains("bundle install"));
338    }
339
340    #[test]
341    fn java_uses_maven_dependency_resolve() {
342        let c = cfg(Language::Java, "packages/java");
343        let install = c.install.unwrap().commands().join(" ");
344        assert!(install.contains("mvn"));
345        assert!(install.contains("dependency:resolve"));
346    }
347
348    #[test]
349    fn csharp_uses_dotnet_restore() {
350        let c = cfg(Language::Csharp, "packages/csharp");
351        let install = c.install.unwrap().commands().join(" ");
352        assert!(install.contains("dotnet restore"));
353    }
354
355    #[test]
356    fn elixir_uses_mix_deps_get() {
357        let c = cfg(Language::Elixir, "packages/elixir");
358        let install = c.install.unwrap().commands().join(" ");
359        assert!(install.contains("mix deps.get"));
360    }
361
362    #[test]
363    fn r_uses_remotes_install_deps() {
364        let c = cfg(Language::R, "packages/r");
365        let install = c.install.unwrap().commands().join(" ");
366        assert!(install.contains("remotes::install_deps()"));
367    }
368
369    #[test]
370    fn output_dir_substituted_in_commands() {
371        let c = cfg(Language::Go, "my/custom/path");
372        let install = c.install.unwrap().commands().join(" ");
373        assert!(install.contains("my/custom/path"));
374    }
375}