1use anyhow::{Context, Result};
18use clap::Args;
19
20use crate::{commands::init as init_cmd, config};
21
22fn is_tty() -> bool {
26 atty::is(atty::Stream::Stdin) && atty::is(atty::Stream::Stdout)
27}
28
29fn resolve_interactive_mode(
39 explicit_interactive: bool,
40 explicit_non_interactive: bool,
41) -> Result<bool> {
42 match (explicit_interactive, explicit_non_interactive) {
43 (true, _) => {
44 if is_tty() {
46 Ok(true)
47 } else {
48 anyhow::bail!(
49 "Interactive mode requested (--interactive) but stdin/stdout is not a TTY. \
50 Use --non-interactive for CI/piped environments."
51 )
52 }
53 }
54 (_, true) => {
55 Ok(false)
57 }
58 (false, false) => {
59 Ok(is_tty())
61 }
62 }
63}
64
65pub fn handle_init(args: InitArgs, force_lock: bool) -> Result<()> {
66 let resolved = if args.check {
67 config::resolve_from_cwd()?
68 } else if args.trust_project_commands {
69 config::resolve_from_cwd_skipping_project_execution_trust()?
70 } else {
71 config::resolve_from_cwd()?
72 };
73
74 if args.check {
77 let check_result = init_cmd::check_readme_current(&resolved)?;
78 match check_result {
79 init_cmd::ReadmeCheckResult::Current(version) => {
80 log::info!("readme: current (version {})", version);
81 return Ok(());
82 }
83 init_cmd::ReadmeCheckResult::Outdated {
84 current_version,
85 embedded_version,
86 } => {
87 log::warn!(
88 "readme: outdated (current version {}, embedded version {})",
89 current_version,
90 embedded_version
91 );
92 log::info!("Run 'ralph init --update-readme' to update");
93 std::process::exit(1);
94 }
95 init_cmd::ReadmeCheckResult::Missing => {
96 log::warn!("readme: missing (would be created on normal init)");
97 std::process::exit(1);
98 }
99 init_cmd::ReadmeCheckResult::NotApplicable => {
100 log::info!("readme: not applicable (prompts don't reference README)");
101 return Ok(());
102 }
103 }
104 }
105
106 let interactive = resolve_interactive_mode(args.interactive, args.non_interactive)
108 .with_context(|| {
109 "Failed to determine interactive mode. \
110 Use --non-interactive for CI/piped environments."
111 })?;
112
113 let report = init_cmd::run_init(
114 &resolved,
115 init_cmd::InitOptions {
116 force: args.force,
117 force_lock,
118 interactive,
119 update_readme: args.update_readme,
120 },
121 )?;
122
123 if args.trust_project_commands {
124 config::initialize_repo_trust_file(&resolved.repo_root)?;
125 }
126
127 fn report_status(label: &str, status: init_cmd::FileInitStatus, path: &std::path::Path) {
128 match status {
129 init_cmd::FileInitStatus::Created => {
130 log::info!("{}: created ({})", label, path.display())
131 }
132 init_cmd::FileInitStatus::Valid => {
133 log::info!("{}: exists (valid) ({})", label, path.display())
134 }
135 init_cmd::FileInitStatus::Updated => {
136 log::info!("{}: updated ({})", label, path.display())
137 }
138 }
139 }
140
141 report_status("queue", report.queue_status, &report.queue_path);
142 report_status("done", report.done_status, &report.done_path);
143 if let Some((status, version_info)) = report.readme_status {
144 let readme_path = resolved.repo_root.join(".ralph/README.md");
145 match status {
146 init_cmd::FileInitStatus::Created => {
147 if let Some(version) = version_info {
148 log::info!(
149 "readme: created (version {}) ({})",
150 version,
151 readme_path.display()
152 );
153 } else {
154 log::info!("readme: created ({})", readme_path.display());
155 }
156 }
157 init_cmd::FileInitStatus::Valid => {
158 if let Some(version) = version_info {
159 log::info!(
160 "readme: exists (version {}) ({})",
161 version,
162 readme_path.display()
163 );
164 } else {
165 log::info!("readme: exists (valid) ({})", readme_path.display());
166 }
167 }
168 init_cmd::FileInitStatus::Updated => {
169 if let Some(version) = version_info {
170 log::info!(
171 "readme: updated (version {}) ({})",
172 version,
173 readme_path.display()
174 );
175 } else {
176 log::info!("readme: updated ({})", readme_path.display());
177 }
178 }
179 }
180 }
181 report_status("config", report.config_status, &report.config_path);
182 Ok(())
183}
184
185#[derive(Args)]
186#[command(
187 about = "Bootstrap Ralph files in the current repository",
188 after_long_help = "Examples:\n ralph init\n ralph init --force\n ralph init --interactive\n ralph init --non-interactive\n ralph init --trust-project-commands\n ralph init --update-readme\n ralph init --check"
189)]
190pub struct InitArgs {
191 #[arg(long)]
193 pub force: bool,
194
195 #[arg(short, long)]
197 pub interactive: bool,
198
199 #[arg(long, conflicts_with = "interactive")]
201 pub non_interactive: bool,
202
203 #[arg(long)]
205 pub update_readme: bool,
206
207 #[arg(long)]
209 pub check: bool,
210
211 #[arg(long = "trust-project-commands", visible_alias = "trust")]
213 pub trust_project_commands: bool,
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn resolve_interactive_mode_explicit_non_interactive() {
222 let result = resolve_interactive_mode(false, true);
224 assert!(result.is_ok());
225 assert!(!result.unwrap());
226 }
227
228 #[test]
229 fn resolve_interactive_mode_explicit_interactive_without_tty() {
230 let result = resolve_interactive_mode(true, false);
233 if !is_tty() {
235 assert!(result.is_err());
236 } else {
237 assert!(result.is_ok());
238 assert!(result.unwrap());
239 }
240 }
241
242 #[test]
243 fn resolve_interactive_mode_auto_detect() {
244 let result = resolve_interactive_mode(false, false);
246 assert!(result.is_ok());
247 assert_eq!(result.unwrap(), is_tty());
250 }
251
252 #[test]
253 fn resolve_interactive_mode_explicit_interactive_wins_over_non_interactive() {
254 let result = resolve_interactive_mode(true, true);
257 if !is_tty() {
260 assert!(result.is_err());
261 } else {
262 assert!(result.is_ok());
263 assert!(result.unwrap());
264 }
265 }
266}