1use std::io::Write as _;
7use std::path::Path;
8use std::process::ExitCode;
9
10use crate::cli::InitArgs;
11use crate::config::{self, ConfigDirective};
12use crate::error::RippyError;
13use crate::packages::Package;
14
15const CARGO_TOML: &str = include_str!("stdlib/cargo.toml");
17const BREW_TOML: &str = include_str!("stdlib/brew.toml");
18const PIP_TOML: &str = include_str!("stdlib/pip.toml");
19const TERRAFORM_TOML: &str = include_str!("stdlib/terraform.toml");
20const PYTEST_TOML: &str = include_str!("stdlib/pytest.toml");
21const MAKE_TOML: &str = include_str!("stdlib/make.toml");
22const RUSTUP_TOML: &str = include_str!("stdlib/rustup.toml");
23const OPENSSL_TOML: &str = include_str!("stdlib/openssl.toml");
24
25const FILE_OPS_TOML: &str = include_str!("stdlib/file_ops.toml");
27
28const BUILTINS_TOML: &str = include_str!("stdlib/builtins.toml");
30const SUDO_TOML: &str = include_str!("stdlib/sudo.toml");
31const SSH_TOML: &str = include_str!("stdlib/ssh.toml");
32const INTERPRETERS_TOML: &str = include_str!("stdlib/interpreters.toml");
33const PACKAGE_MANAGERS_TOML: &str = include_str!("stdlib/package_managers.toml");
34
35const STDLIB_SOURCES: &[(&str, &str)] = &[
37 ("(stdlib:cargo)", CARGO_TOML),
39 ("(stdlib:brew)", BREW_TOML),
40 ("(stdlib:pip)", PIP_TOML),
41 ("(stdlib:terraform)", TERRAFORM_TOML),
42 ("(stdlib:pytest)", PYTEST_TOML),
43 ("(stdlib:make)", MAKE_TOML),
44 ("(stdlib:rustup)", RUSTUP_TOML),
45 ("(stdlib:openssl)", OPENSSL_TOML),
46 ("(stdlib:file_ops)", FILE_OPS_TOML),
48 ("(stdlib:builtins)", BUILTINS_TOML),
50 ("(stdlib:sudo)", SUDO_TOML),
51 ("(stdlib:ssh)", SSH_TOML),
52 ("(stdlib:interpreters)", INTERPRETERS_TOML),
53 ("(stdlib:package_managers)", PACKAGE_MANAGERS_TOML),
54];
55
56pub fn stdlib_directives() -> Result<Vec<ConfigDirective>, RippyError> {
62 let mut directives = Vec::new();
63 for (label, source) in STDLIB_SOURCES {
64 let parsed = crate::toml_config::parse_toml_config(source, Path::new(label))?;
65 directives.extend(parsed);
66 }
67 Ok(directives)
68}
69
70#[must_use]
72pub fn stdlib_toml() -> String {
73 let mut out = String::new();
74 for (_, source) in STDLIB_SOURCES {
75 out.push_str(source);
76 out.push('\n');
77 }
78 out
79}
80
81pub fn run_init(args: &InitArgs) -> Result<ExitCode, RippyError> {
87 if args.stdout {
88 print!("{}", stdlib_toml());
89 return Ok(ExitCode::SUCCESS);
90 }
91
92 let package = resolve_init_package(args)?;
93
94 let path = if args.global {
95 config::home_dir()
96 .map(|h| h.join(".rippy/config.toml"))
97 .ok_or_else(|| RippyError::Setup("could not determine home directory".into()))?
98 } else {
99 std::path::PathBuf::from(".rippy.toml")
100 };
101
102 if path.exists() {
103 return Err(RippyError::Setup(format!(
104 "{} already exists. Remove it first or edit manually.",
105 path.display()
106 )));
107 }
108
109 crate::profile_cmd::write_package_setting(&path, package.name())?;
110
111 if !args.global {
112 crate::trust::TrustGuard::for_new_file(&path).commit();
113 }
114
115 eprintln!(
116 "[rippy] Created {} with package \"{}\"\n \
117 \"{}\"\n \
118 Run `rippy profile show {}` for details, or edit {} to customize.",
119 path.display(),
120 package.name(),
121 package.tagline(),
122 package.name(),
123 path.display(),
124 );
125 Ok(ExitCode::SUCCESS)
126}
127
128fn resolve_init_package(args: &InitArgs) -> Result<Package, RippyError> {
131 if let Some(name) = &args.package {
132 let home = config::home_dir();
133 return Package::resolve(name, home.as_deref());
134 }
135
136 if is_interactive() {
137 return prompt_package_selection();
138 }
139
140 Ok(Package::Develop)
142}
143
144fn is_interactive() -> bool {
145 use std::io::IsTerminal;
146 std::io::stdin().is_terminal()
147}
148
149fn prompt_package_selection() -> Result<Package, RippyError> {
150 let packages = Package::all();
151 let default_idx = packages
152 .iter()
153 .position(|p| *p == Package::Develop)
154 .unwrap_or(0);
155
156 eprintln!("\nWhich package fits your workflow?\n");
157 for (i, pkg) in packages.iter().enumerate() {
158 let recommended = if i == default_idx {
159 " (recommended)"
160 } else {
161 ""
162 };
163 eprintln!(
164 " [{}] {:<12}[{}] {}{recommended}",
165 i + 1,
166 pkg.name(),
167 pkg.shield(),
168 pkg.tagline(),
169 );
170 }
171 eprint!(
172 "\nSelect [1-{}] (default: {}): ",
173 packages.len(),
174 default_idx + 1
175 );
176 let _ = std::io::stderr().flush();
177
178 let mut input = String::new();
179 if std::io::stdin().read_line(&mut input).is_err() {
180 return Ok(packages[default_idx].clone());
181 }
182
183 let trimmed = input.trim();
184 if trimmed.is_empty() {
185 return Ok(packages[default_idx].clone());
186 }
187
188 if let Ok(n) = trimmed.parse::<usize>()
190 && n >= 1
191 && n <= packages.len()
192 {
193 return Ok(packages[n - 1].clone());
194 }
195
196 Package::parse(trimmed).map_err(RippyError::Setup)
197}
198
199#[cfg(test)]
200#[allow(clippy::unwrap_used)]
201mod tests {
202 use super::*;
203 use crate::config::Config;
204 use crate::verdict::Decision;
205
206 #[test]
207 fn stdlib_parses_without_error() {
208 let directives = stdlib_directives().unwrap();
209 assert!(!directives.is_empty());
210 }
211
212 #[test]
213 fn stdlib_cargo_safe_subcommands() {
214 let config = Config::from_directives(stdlib_directives().unwrap());
215 let v = config.match_command("cargo test --release", None);
216 assert!(v.is_some());
217 assert_eq!(v.unwrap().decision, Decision::Allow);
218 }
219
220 #[test]
221 fn stdlib_cargo_ask_subcommands() {
222 let config = Config::from_directives(stdlib_directives().unwrap());
223 let v = config.match_command("cargo run", None);
224 assert!(v.is_some());
225 assert_eq!(v.unwrap().decision, Decision::Ask);
226 }
227
228 #[test]
229 fn stdlib_cargo_unknown_defaults_to_ask() {
230 let config = Config::from_directives(stdlib_directives().unwrap());
231 let v = config.match_command("cargo some-unknown-subcommand", None);
232 assert!(v.is_some());
233 assert_eq!(v.unwrap().decision, Decision::Ask);
234 }
235
236 #[test]
237 fn stdlib_file_ops_ask() {
238 let config = Config::from_directives(stdlib_directives().unwrap());
239 for cmd in &["rm -rf /tmp/test", "mv a b", "chmod 755 file"] {
240 let v = config.match_command(cmd, None);
241 assert!(v.is_some(), "expected match for {cmd}");
242 assert_eq!(v.unwrap().decision, Decision::Ask, "expected ask for {cmd}");
243 }
244 }
245
246 #[test]
247 fn stdlib_dangerous_commands_ask() {
248 let config = Config::from_directives(stdlib_directives().unwrap());
249 for cmd in &["sudo apt install foo", "ssh user@host", "eval echo hi"] {
250 let v = config.match_command(cmd, None);
251 assert!(v.is_some(), "expected match for {cmd}");
252 assert_eq!(v.unwrap().decision, Decision::Ask, "expected ask for {cmd}");
253 }
254 }
255
256 #[test]
257 fn stdlib_toml_not_empty() {
258 let toml = stdlib_toml();
259 assert!(toml.contains("[[rules]]"));
260 assert!(toml.contains("cargo"));
261 }
262
263 #[test]
264 fn init_refuses_existing_file() {
265 let dir = tempfile::TempDir::new().unwrap();
266 let path = dir.path().join(".rippy.toml");
267 std::fs::write(&path, "existing").unwrap();
268
269 let original = std::env::current_dir().unwrap();
270 std::env::set_current_dir(dir.path()).unwrap();
271 let result = run_init(&InitArgs {
272 global: false,
273 stdout: false,
274 package: Some("develop".into()),
275 });
276 std::env::set_current_dir(original).unwrap();
277
278 assert!(result.is_err());
279 }
280}