1use std::path::PathBuf;
2
3use crate::cli::{CliArgs, Command};
4use crate::edit::FlakeEdit;
5use crate::tui;
6
7use super::commands::follow;
8use super::commands::{self};
9use super::editor::Editor;
10use super::error::{Error, Result};
11use super::state::AppState;
12
13pub(crate) mod root;
14
15pub fn run(args: CliArgs) -> Result<()> {
19 if let Command::Follow {
20 paths,
21 transitive,
22 depth,
23 stats,
24 dry_run,
25 } = args.subcommand()
26 && !paths.is_empty()
27 {
28 if args.flake().is_some() || args.lock_file().is_some() {
29 return Err(Error::IncompatibleFollowOptions);
30 }
31 let report = follow::auto::ReportMode::from_flags(*stats, *dry_run);
32 return follow::auto::run_batch(paths, *transitive, *depth, report, &args);
33 }
34
35 let (editor, mut flake_edit, mut state) = setup(&args)?;
36 let no_cache = args.no_cache();
37
38 match args.subcommand() {
39 Command::Add { .. } => dispatch_add(&args, &editor, &mut flake_edit, &state)?,
40 Command::Remove { .. } => dispatch_remove(&args, &editor, &mut flake_edit, &state)?,
41 Command::Change { .. } => dispatch_change(&args, &editor, &mut flake_edit, &state)?,
42 Command::List { .. } => dispatch_list(&args, &mut flake_edit)?,
43 Command::Update { .. } => dispatch_update(&args, &editor, &mut flake_edit, &state)?,
44 Command::Pin { .. } => dispatch_pin(&args, &editor, &mut flake_edit, &state)?,
45 Command::Unpin { .. } => dispatch_unpin(&args, &editor, &mut flake_edit, &state)?,
46 Command::Toggle { .. } => dispatch_toggle(&args, &editor, &mut flake_edit, &state)?,
47 Command::Follow { .. } => dispatch_follow(&args, &editor, &mut flake_edit, &mut state)?,
48 Command::AddFollow { .. } => {
49 dispatch_add_follow(&args, &editor, &mut flake_edit, &mut state)?
50 }
51 Command::Config { .. } => return dispatch_config(&args),
52 }
53
54 crate::cache::populate_cache_from_input_map(flake_edit.curr_list(), no_cache);
55
56 Ok(())
57}
58
59fn setup(args: &CliArgs) -> Result<(Editor, FlakeEdit, AppState)> {
60 let flake_path = if let Some(flake) = args.flake() {
61 let path = PathBuf::from(flake);
62 if path.is_dir() {
63 let flake_nix = path.join("flake.nix");
64 if !flake_nix.exists() {
65 return Err(Error::FlakeDirEmpty { path });
66 }
67 flake_nix
68 } else {
69 path
70 }
71 } else {
72 let path = PathBuf::from("flake.nix");
73 let binding = root::Root::from_path(&path).map_err(|source| Error::FlakeNotFound {
74 path: path.clone(),
75 source,
76 })?;
77 binding.path().to_path_buf()
78 };
79
80 let editor = Editor::from_path(flake_path.clone()).map_err(|source| Error::FlakeNotFound {
81 path: flake_path.clone(),
82 source,
83 })?;
84 let flake_edit = editor.create_flake_edit()?;
85 let interactive = tui::is_interactive(args.non_interactive());
86
87 let state = AppState::new(flake_path, args.config().map(PathBuf::from))?
88 .with_diff(args.diff())
89 .with_no_lock(args.no_lock())
90 .with_interactive(interactive)
91 .with_lock_file(args.lock_file().map(PathBuf::from))
92 .with_no_cache(args.no_cache())
93 .with_cache_path(args.cache().map(PathBuf::from));
94
95 Ok((editor, flake_edit, state))
96}
97
98fn dispatch_add(
99 args: &CliArgs,
100 editor: &Editor,
101 flake_edit: &mut FlakeEdit,
102 state: &AppState,
103) -> Result<()> {
104 let Command::Add {
105 uri,
106 ref_or_rev,
107 id,
108 no_flake,
109 shallow,
110 } = args.subcommand()
111 else {
112 unreachable!("wrong Command variant");
113 };
114 commands::add(
115 editor,
116 flake_edit,
117 state,
118 id.clone(),
119 uri.clone(),
120 *no_flake,
121 commands::UriOptions {
122 ref_or_rev: ref_or_rev.as_deref(),
123 shallow: *shallow,
124 },
125 )
126}
127
128fn dispatch_remove(
129 args: &CliArgs,
130 editor: &Editor,
131 flake_edit: &mut FlakeEdit,
132 state: &AppState,
133) -> Result<()> {
134 let Command::Remove { id } = args.subcommand() else {
135 unreachable!("wrong Command variant");
136 };
137 commands::remove(editor, flake_edit, state, id.clone())
138}
139
140fn dispatch_change(
141 args: &CliArgs,
142 editor: &Editor,
143 flake_edit: &mut FlakeEdit,
144 state: &AppState,
145) -> Result<()> {
146 let Command::Change {
147 uri,
148 ref_or_rev,
149 id,
150 shallow,
151 } = args.subcommand()
152 else {
153 unreachable!("wrong Command variant");
154 };
155 commands::change(
156 editor,
157 flake_edit,
158 state,
159 id.clone(),
160 uri.clone(),
161 commands::UriOptions {
162 ref_or_rev: ref_or_rev.as_deref(),
163 shallow: *shallow,
164 },
165 )
166}
167
168fn dispatch_list(args: &CliArgs, flake_edit: &mut FlakeEdit) -> Result<()> {
169 let Command::List { format } = args.subcommand() else {
170 unreachable!("wrong Command variant");
171 };
172 commands::list(flake_edit, format)
173}
174
175fn dispatch_update(
176 args: &CliArgs,
177 editor: &Editor,
178 flake_edit: &mut FlakeEdit,
179 state: &AppState,
180) -> Result<()> {
181 let Command::Update { id, init } = args.subcommand() else {
182 unreachable!("wrong Command variant");
183 };
184 commands::update(editor, flake_edit, state, id.clone(), *init)
185}
186
187fn dispatch_pin(
188 args: &CliArgs,
189 editor: &Editor,
190 flake_edit: &mut FlakeEdit,
191 state: &AppState,
192) -> Result<()> {
193 let Command::Pin { id, rev } = args.subcommand() else {
194 unreachable!("wrong Command variant");
195 };
196 commands::pin(editor, flake_edit, state, id.clone(), rev.clone())
197}
198
199fn dispatch_unpin(
200 args: &CliArgs,
201 editor: &Editor,
202 flake_edit: &mut FlakeEdit,
203 state: &AppState,
204) -> Result<()> {
205 let Command::Unpin { id } = args.subcommand() else {
206 unreachable!("wrong Command variant");
207 };
208 commands::unpin(editor, flake_edit, state, id.clone())
209}
210
211fn dispatch_toggle(
212 args: &CliArgs,
213 editor: &Editor,
214 flake_edit: &mut FlakeEdit,
215 state: &AppState,
216) -> Result<()> {
217 let Command::Toggle {
218 input,
219 reference,
220 remove,
221 } = args.subcommand()
222 else {
223 unreachable!("wrong Command variant");
224 };
225 commands::toggle(
226 editor,
227 flake_edit,
228 state,
229 input.clone(),
230 reference.clone(),
231 *remove,
232 )
233}
234
235fn dispatch_follow(
236 args: &CliArgs,
237 editor: &Editor,
238 flake_edit: &mut FlakeEdit,
239 state: &mut AppState,
240) -> Result<()> {
241 let Command::Follow {
242 paths: _,
243 transitive,
244 depth,
245 stats,
246 dry_run,
247 } = args.subcommand()
248 else {
249 unreachable!("wrong Command variant");
250 };
251 if let Some(min) = transitive {
252 state.config.follow.transitive_min = *min;
253 }
254 if let Some(max) = depth {
255 state.config.follow.max_depth = Some(*max);
256 }
257 state.lock_offline = true;
258 follow::auto::run(
259 editor,
260 flake_edit,
261 state,
262 follow::auto::ReportMode::from_flags(*stats, *dry_run),
263 )
264}
265
266fn dispatch_add_follow(
267 args: &CliArgs,
268 editor: &Editor,
269 flake_edit: &mut FlakeEdit,
270 state: &mut AppState,
271) -> Result<()> {
272 let Command::AddFollow { input, target } = args.subcommand() else {
273 unreachable!("wrong Command variant");
274 };
275 state.lock_offline = true;
276 follow::add_follow(editor, flake_edit, state, input.clone(), target.clone())
277}
278
279fn dispatch_config(args: &CliArgs) -> Result<()> {
280 let Command::Config {
281 print_default,
282 path,
283 } = args.subcommand()
284 else {
285 unreachable!("wrong Command variant");
286 };
287 commands::config(*print_default, *path)
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293 use clap::Parser;
294
295 const MINIMAL_FLAKE: &str = "{\n inputs = {};\n outputs = { self }: { };\n}\n";
296
297 fn write_minimal_flake(dir: &std::path::Path) -> std::path::PathBuf {
298 let path = dir.join("flake.nix");
299 std::fs::write(&path, MINIMAL_FLAKE).expect("write flake.nix");
300 path
301 }
302
303 fn parse(args: &[&str]) -> CliArgs {
304 CliArgs::try_parse_from(args).expect("parse CLI args")
305 }
306
307 #[test]
308 fn batch_follow_with_flake_flag_is_rejected() {
309 let args = parse(&[
310 "flake-edit",
311 "--flake",
312 "/does/not/exist/flake.nix",
313 "follow",
314 "/some/path/flake.nix",
315 ]);
316 let err = run(args).expect_err("batch follow + --flake must be rejected");
317 assert!(matches!(err, Error::IncompatibleFollowOptions));
318 }
319
320 #[test]
321 fn batch_follow_with_lock_file_flag_is_rejected() {
322 let args = parse(&[
323 "flake-edit",
324 "--lock-file",
325 "/does/not/exist/flake.lock",
326 "follow",
327 "/some/path/flake.nix",
328 ]);
329 let err = run(args).expect_err("batch follow + --lock-file must be rejected");
330 assert!(matches!(err, Error::IncompatibleFollowOptions));
331 }
332
333 #[test]
334 fn flake_dir_without_flake_nix_returns_flake_dir_empty() {
335 let tmp = tempfile::tempdir().expect("tempdir");
336 let args = parse(&[
337 "flake-edit",
338 "--flake",
339 tmp.path().to_str().unwrap(),
340 "list",
341 ]);
342 let err = run(args).expect_err("empty dir must yield FlakeDirEmpty");
343 assert!(matches!(err, Error::FlakeDirEmpty { .. }));
344 }
345
346 #[test]
347 fn missing_flake_file_returns_flake_not_found() {
348 let tmp = tempfile::tempdir().expect("tempdir");
349 let missing = tmp.path().join("missing.nix");
350 let args = parse(&["flake-edit", "--flake", missing.to_str().unwrap(), "list"]);
351 let err = run(args).expect_err("missing file must yield FlakeNotFound");
352 assert!(matches!(err, Error::FlakeNotFound { .. }));
353 }
354
355 #[test]
356 fn config_print_default_does_not_touch_flake_nix() {
357 let tmp = tempfile::tempdir().expect("tempdir");
358 let flake = write_minimal_flake(tmp.path());
359 let args = parse(&[
360 "flake-edit",
361 "--flake",
362 tmp.path().to_str().unwrap(),
363 "--non-interactive",
364 "--no-cache",
365 "config",
366 "--print-default",
367 ]);
368 run(args).expect("config --print-default must succeed");
369 assert_eq!(
370 std::fs::read_to_string(&flake).expect("read flake.nix"),
371 MINIMAL_FLAKE,
372 "config --print-default must not rewrite flake.nix",
373 );
374 }
375}