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