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 = config::resolve_from_cwd()?;
67
68 if args.check {
71 let check_result = init_cmd::check_readme_current(&resolved)?;
72 match check_result {
73 init_cmd::ReadmeCheckResult::Current(version) => {
74 log::info!("readme: current (version {})", version);
75 return Ok(());
76 }
77 init_cmd::ReadmeCheckResult::Outdated {
78 current_version,
79 embedded_version,
80 } => {
81 log::warn!(
82 "readme: outdated (current version {}, embedded version {})",
83 current_version,
84 embedded_version
85 );
86 log::info!("Run 'ralph init --update-readme' to update");
87 std::process::exit(1);
88 }
89 init_cmd::ReadmeCheckResult::Missing => {
90 log::warn!("readme: missing (would be created on normal init)");
91 std::process::exit(1);
92 }
93 init_cmd::ReadmeCheckResult::NotApplicable => {
94 log::info!("readme: not applicable (prompts don't reference README)");
95 return Ok(());
96 }
97 }
98 }
99
100 let interactive = resolve_interactive_mode(args.interactive, args.non_interactive)
102 .with_context(|| {
103 "Failed to determine interactive mode. \
104 Use --non-interactive for CI/piped environments."
105 })?;
106
107 let report = init_cmd::run_init(
108 &resolved,
109 init_cmd::InitOptions {
110 force: args.force,
111 force_lock,
112 interactive,
113 update_readme: args.update_readme,
114 },
115 )?;
116
117 fn report_status(label: &str, status: init_cmd::FileInitStatus, path: &std::path::Path) {
118 match status {
119 init_cmd::FileInitStatus::Created => {
120 log::info!("{}: created ({})", label, path.display())
121 }
122 init_cmd::FileInitStatus::Valid => {
123 log::info!("{}: exists (valid) ({})", label, path.display())
124 }
125 init_cmd::FileInitStatus::Updated => {
126 log::info!("{}: updated ({})", label, path.display())
127 }
128 }
129 }
130
131 report_status("queue", report.queue_status, &report.queue_path);
132 report_status("done", report.done_status, &report.done_path);
133 if let Some((status, version_info)) = report.readme_status {
134 let readme_path = resolved.repo_root.join(".ralph/README.md");
135 match status {
136 init_cmd::FileInitStatus::Created => {
137 if let Some(version) = version_info {
138 log::info!(
139 "readme: created (version {}) ({})",
140 version,
141 readme_path.display()
142 );
143 } else {
144 log::info!("readme: created ({})", readme_path.display());
145 }
146 }
147 init_cmd::FileInitStatus::Valid => {
148 if let Some(version) = version_info {
149 log::info!(
150 "readme: exists (version {}) ({})",
151 version,
152 readme_path.display()
153 );
154 } else {
155 log::info!("readme: exists (valid) ({})", readme_path.display());
156 }
157 }
158 init_cmd::FileInitStatus::Updated => {
159 if let Some(version) = version_info {
160 log::info!(
161 "readme: updated (version {}) ({})",
162 version,
163 readme_path.display()
164 );
165 } else {
166 log::info!("readme: updated ({})", readme_path.display());
167 }
168 }
169 }
170 }
171 report_status("config", report.config_status, &report.config_path);
172 Ok(())
173}
174
175#[derive(Args)]
176#[command(
177 about = "Bootstrap Ralph files in the current repository",
178 after_long_help = "Examples:\n ralph init\n ralph init --force\n ralph init --interactive\n ralph init --non-interactive\n ralph init --update-readme\n ralph init --check"
179)]
180pub struct InitArgs {
181 #[arg(long)]
183 pub force: bool,
184
185 #[arg(short, long)]
187 pub interactive: bool,
188
189 #[arg(long, conflicts_with = "interactive")]
191 pub non_interactive: bool,
192
193 #[arg(long)]
195 pub update_readme: bool,
196
197 #[arg(long)]
199 pub check: bool,
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn resolve_interactive_mode_explicit_non_interactive() {
208 let result = resolve_interactive_mode(false, true);
210 assert!(result.is_ok());
211 assert!(!result.unwrap());
212 }
213
214 #[test]
215 fn resolve_interactive_mode_explicit_interactive_without_tty() {
216 let result = resolve_interactive_mode(true, false);
219 if !is_tty() {
221 assert!(result.is_err());
222 } else {
223 assert!(result.is_ok());
224 assert!(result.unwrap());
225 }
226 }
227
228 #[test]
229 fn resolve_interactive_mode_auto_detect() {
230 let result = resolve_interactive_mode(false, false);
232 assert!(result.is_ok());
233 assert_eq!(result.unwrap(), is_tty());
236 }
237
238 #[test]
239 fn resolve_interactive_mode_explicit_interactive_wins_over_non_interactive() {
240 let result = resolve_interactive_mode(true, true);
243 if !is_tty() {
246 assert!(result.is_err());
247 } else {
248 assert!(result.is_ok());
249 assert!(result.unwrap());
250 }
251 }
252}