fob_cli/commands/
utils.rs1use crate::error::{BuildError, CliError, Result};
12use std::fs;
13use std::path::{Path, PathBuf};
14
15pub fn resolve_path(path: &Path, cwd: &Path) -> PathBuf {
29 if path.is_absolute() {
30 path.to_path_buf()
31 } else {
32 cwd.join(path)
33 }
34}
35
36pub fn validate_entry(entry: &Path) -> Result<()> {
46 if !entry.exists() {
47 return Err(BuildError::EntryNotFound(entry.to_path_buf()).into());
48 }
49
50 if !entry.is_file() {
51 return Err(CliError::InvalidArgument(format!(
52 "Entry point is not a file: {}",
53 entry.display()
54 )));
55 }
56
57 Ok(())
58}
59
60pub fn clean_output_dir(out_dir: &Path) -> Result<()> {
79 if out_dir.exists() {
80 if !out_dir.is_dir() {
81 return Err(CliError::InvalidArgument(format!(
82 "Output path exists but is not a directory: {}",
83 out_dir.display()
84 )));
85 }
86
87 for entry in fs::read_dir(out_dir)? {
89 let entry = entry?;
90 let path = entry.path();
91
92 if path.is_dir() {
93 fs::remove_dir_all(&path)?;
94 } else {
95 fs::remove_file(&path)?;
96 }
97 }
98 } else {
99 fs::create_dir_all(out_dir)?;
101 }
102
103 Ok(())
104}
105
106pub fn ensure_output_dir(out_dir: &Path) -> Result<()> {
116 if !out_dir.exists() {
117 fs::create_dir_all(out_dir)?;
118 } else if !out_dir.is_dir() {
119 return Err(CliError::InvalidArgument(format!(
120 "Output path exists but is not a directory: {}",
121 out_dir.display()
122 )));
123 }
124
125 Ok(())
126}
127
128pub fn get_cwd() -> Result<PathBuf> {
134 std::env::current_dir().map_err(|e| {
135 CliError::Io(std::io::Error::new(
136 e.kind(),
137 format!("Failed to get current directory: {}", e),
138 ))
139 })
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154pub enum PackageManager {
155 Npm,
156 Yarn,
157 Pnpm,
158 Bun,
159}
160
161impl PackageManager {
162 pub fn detect(project_dir: &Path) -> Self {
170 if project_dir.join("pnpm-lock.yaml").exists() {
171 PackageManager::Pnpm
172 } else if project_dir.join("yarn.lock").exists() {
173 PackageManager::Yarn
174 } else if project_dir.join("bun.lockb").exists() {
175 PackageManager::Bun
176 } else {
177 PackageManager::Npm
178 }
179 }
180
181 pub fn command(&self) -> &'static str {
183 match self {
184 PackageManager::Npm => "npm",
185 PackageManager::Yarn => "yarn",
186 PackageManager::Pnpm => "pnpm",
187 PackageManager::Bun => "bun",
188 }
189 }
190
191 pub fn install_cmd(&self) -> &'static str {
193 match self {
194 PackageManager::Npm => "npm install",
195 PackageManager::Yarn => "yarn install",
196 PackageManager::Pnpm => "pnpm install",
197 PackageManager::Bun => "bun install",
198 }
199 }
200}
201
202impl std::fmt::Display for PackageManager {
203 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204 write!(f, "{}", self.command())
205 }
206}
207
208pub fn find_package_json(start_dir: &Path) -> Option<PathBuf> {
229 let mut current = start_dir;
230
231 loop {
232 let package_json_path = current.join("package.json");
233
234 if package_json_path.exists() && package_json_path.is_file() {
235 return Some(current.to_path_buf());
236 }
237
238 match current.parent() {
240 Some(parent) => current = parent,
241 None => {
242 return None;
244 }
245 }
246 }
247}
248
249pub fn resolve_project_root(
284 explicit_cwd: Option<&Path>,
285 entry_point: Option<&str>,
286) -> Result<PathBuf> {
287 use crate::ui;
288
289 if let Some(cwd_path) = explicit_cwd {
291 let absolute = if cwd_path.is_absolute() {
292 cwd_path.to_path_buf()
293 } else {
294 std::env::current_dir()
295 .map_err(CliError::Io)?
296 .join(cwd_path)
297 };
298
299 if !absolute.exists() {
300 return Err(CliError::InvalidArgument(format!(
301 "Specified --cwd directory does not exist: {}",
302 absolute.display()
303 )));
304 }
305
306 if !absolute.is_dir() {
307 return Err(CliError::InvalidArgument(format!(
308 "Specified --cwd is not a directory: {}",
309 absolute.display()
310 )));
311 }
312
313 ui::info(&format!(
314 "Using project root: {} (from --cwd flag)",
315 absolute.display()
316 ));
317 return Ok(absolute);
318 }
319
320 if let Some(entry) = entry_point {
322 let current_dir = std::env::current_dir().map_err(CliError::Io)?;
323
324 let entry_path = if Path::new(entry).is_absolute() {
325 PathBuf::from(entry)
326 } else {
327 current_dir.join(entry)
328 };
329
330 if let Some(entry_dir) = entry_path.parent() {
332 if let Some(package_root) = find_package_json(entry_dir) {
333 ui::info(&format!(
334 "Using project root: {} (detected from entry point's package.json)",
335 package_root.display()
336 ));
337 return Ok(package_root);
338 }
339 }
340 }
341
342 let current_dir = std::env::current_dir().map_err(CliError::Io)?;
344
345 if let Some(package_root) = find_package_json(¤t_dir) {
346 ui::info(&format!(
347 "Using project root: {} (auto-detected from package.json)",
348 package_root.display()
349 ));
350 return Ok(package_root);
351 }
352
353 ui::warning(&format!(
355 "No package.json found. Using current directory: {}",
356 current_dir.display()
357 ));
358 ui::info("Consider using --cwd to specify your project root explicitly.");
359
360 Ok(current_dir)
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366 use std::fs::File;
367 use tempfile::TempDir;
368
369 #[test]
370 fn test_resolve_path_absolute() {
371 let abs_path = PathBuf::from("/absolute/path");
372 let cwd = PathBuf::from("/some/dir");
373
374 let resolved = resolve_path(&abs_path, &cwd);
375 assert_eq!(resolved, abs_path);
376 }
377
378 #[test]
379 fn test_resolve_path_relative() {
380 let rel_path = PathBuf::from("relative/path");
381 let cwd = PathBuf::from("/some/dir");
382
383 let resolved = resolve_path(&rel_path, &cwd);
384 assert_eq!(resolved, PathBuf::from("/some/dir/relative/path"));
385 }
386
387 #[test]
388 fn test_validate_entry_exists() {
389 let temp_dir = TempDir::new().unwrap();
390 let entry_path = temp_dir.path().join("index.ts");
391 File::create(&entry_path).unwrap();
392
393 assert!(validate_entry(&entry_path).is_ok());
394 }
395
396 #[test]
397 fn test_validate_entry_not_found() {
398 let temp_dir = TempDir::new().unwrap();
399 let entry_path = temp_dir.path().join("nonexistent.ts");
400
401 let result = validate_entry(&entry_path);
402 assert!(result.is_err());
403 assert!(matches!(
404 result.unwrap_err(),
405 CliError::Build(BuildError::EntryNotFound { .. })
406 ));
407 }
408
409 #[test]
410 fn test_validate_entry_not_file() {
411 let temp_dir = TempDir::new().unwrap();
412
413 let result = validate_entry(temp_dir.path());
414 assert!(result.is_err());
415 }
416
417 #[test]
418 fn test_clean_output_dir_creates_if_missing() {
419 let temp_dir = TempDir::new().unwrap();
420 let out_dir = temp_dir.path().join("dist");
421
422 assert!(!out_dir.exists());
423 clean_output_dir(&out_dir).unwrap();
424 assert!(out_dir.exists());
425 assert!(out_dir.is_dir());
426 }
427
428 #[test]
429 fn test_clean_output_dir_removes_contents() {
430 let temp_dir = TempDir::new().unwrap();
431 let out_dir = temp_dir.path().join("dist");
432 fs::create_dir(&out_dir).unwrap();
433
434 File::create(out_dir.join("file1.js")).unwrap();
436 File::create(out_dir.join("file2.js")).unwrap();
437 fs::create_dir(out_dir.join("subdir")).unwrap();
438 File::create(out_dir.join("subdir/file3.js")).unwrap();
439
440 clean_output_dir(&out_dir).unwrap();
441
442 assert!(out_dir.exists());
444 assert!(out_dir.is_dir());
445 assert_eq!(fs::read_dir(&out_dir).unwrap().count(), 0);
446 }
447
448 #[test]
449 fn test_clean_output_dir_not_a_directory() {
450 let temp_dir = TempDir::new().unwrap();
451 let file_path = temp_dir.path().join("not_a_dir");
452 File::create(&file_path).unwrap();
453
454 let result = clean_output_dir(&file_path);
455 assert!(result.is_err());
456 }
457
458 #[test]
459 fn test_ensure_output_dir_creates() {
460 let temp_dir = TempDir::new().unwrap();
461 let out_dir = temp_dir.path().join("new_dir");
462
463 ensure_output_dir(&out_dir).unwrap();
464 assert!(out_dir.exists());
465 assert!(out_dir.is_dir());
466 }
467
468 #[test]
469 fn test_ensure_output_dir_exists() {
470 let temp_dir = TempDir::new().unwrap();
471 let out_dir = temp_dir.path().join("existing");
472 fs::create_dir(&out_dir).unwrap();
473
474 ensure_output_dir(&out_dir).unwrap();
476 }
477
478 #[test]
479 fn test_get_cwd() {
480 let cwd = get_cwd().unwrap();
481 assert!(cwd.is_absolute());
482 }
483
484 #[test]
485 fn test_package_manager_detect_pnpm() {
486 let temp_dir = TempDir::new().unwrap();
487 File::create(temp_dir.path().join("pnpm-lock.yaml")).unwrap();
488
489 assert_eq!(
490 PackageManager::detect(temp_dir.path()),
491 PackageManager::Pnpm
492 );
493 }
494
495 #[test]
496 fn test_package_manager_detect_yarn() {
497 let temp_dir = TempDir::new().unwrap();
498 File::create(temp_dir.path().join("yarn.lock")).unwrap();
499
500 assert_eq!(
501 PackageManager::detect(temp_dir.path()),
502 PackageManager::Yarn
503 );
504 }
505
506 #[test]
507 fn test_package_manager_detect_npm() {
508 let temp_dir = TempDir::new().unwrap();
509 assert_eq!(PackageManager::detect(temp_dir.path()), PackageManager::Npm);
512 }
513
514 #[test]
515 fn test_package_manager_pnpm_prefers_over_yarn() {
516 let temp_dir = TempDir::new().unwrap();
517 File::create(temp_dir.path().join("pnpm-lock.yaml")).unwrap();
518 File::create(temp_dir.path().join("yarn.lock")).unwrap();
519
520 assert_eq!(
522 PackageManager::detect(temp_dir.path()),
523 PackageManager::Pnpm
524 );
525 }
526
527 #[test]
528 fn test_package_manager_commands() {
529 assert_eq!(PackageManager::Npm.command(), "npm");
530 assert_eq!(PackageManager::Yarn.command(), "yarn");
531 assert_eq!(PackageManager::Pnpm.command(), "pnpm");
532 }
533
534 #[test]
535 fn test_package_manager_install_cmd() {
536 assert_eq!(PackageManager::Npm.install_cmd(), "npm install");
537 assert_eq!(PackageManager::Yarn.install_cmd(), "yarn install");
538 assert_eq!(PackageManager::Pnpm.install_cmd(), "pnpm install");
539 }
540
541 #[test]
542 fn test_find_package_json_in_current_dir() {
543 let temp = TempDir::new().unwrap();
544 let package_json = temp.path().join("package.json");
545 File::create(&package_json).unwrap();
546
547 let result = find_package_json(temp.path());
548 assert_eq!(result, Some(temp.path().to_path_buf()));
549 }
550
551 #[test]
552 fn test_find_package_json_walks_up() {
553 let temp = TempDir::new().unwrap();
554 let package_json = temp.path().join("package.json");
555 File::create(&package_json).unwrap();
556
557 let nested = temp.path().join("src").join("components");
558 fs::create_dir_all(&nested).unwrap();
559
560 let result = find_package_json(&nested);
561 assert_eq!(result, Some(temp.path().to_path_buf()));
562 }
563
564 #[test]
565 fn test_find_package_json_stops_at_first() {
566 let temp = TempDir::new().unwrap();
567
568 let root_package = temp.path().join("package.json");
570 File::create(&root_package).unwrap();
571
572 let nested = temp.path().join("packages").join("app");
573 fs::create_dir_all(&nested).unwrap();
574 let nested_package = nested.join("package.json");
575 File::create(&nested_package).unwrap();
576
577 let result = find_package_json(&nested);
579 assert_eq!(result, Some(nested.clone()));
580 }
581
582 #[test]
583 fn test_resolve_project_root_explicit_cwd() {
584 let temp = TempDir::new().unwrap();
585
586 let result = resolve_project_root(Some(temp.path()), None).unwrap();
587 assert_eq!(result, temp.path());
588 }
589
590 #[test]
591 fn test_resolve_project_root_explicit_cwd_invalid() {
592 let invalid_path = Path::new("/this/path/definitely/does/not/exist/12345");
593
594 let result = resolve_project_root(Some(invalid_path), None);
595 assert!(result.is_err());
596 assert!(result.unwrap_err().to_string().contains("does not exist"));
597 }
598
599 #[test]
600 fn test_resolve_project_root_from_entry() {
601 let temp = TempDir::new().unwrap();
602 let package_json = temp.path().join("package.json");
603 File::create(&package_json).unwrap();
604
605 let src = temp.path().join("src");
606 fs::create_dir_all(&src).unwrap();
607 let entry = src.join("index.ts");
608 File::create(&entry).unwrap();
609
610 let original = std::env::current_dir().unwrap();
612 std::env::set_current_dir(temp.path()).unwrap();
613
614 let result = resolve_project_root(None, Some("src/index.ts")).unwrap();
615 let expected = temp.path().canonicalize().unwrap();
617 assert_eq!(result.canonicalize().unwrap(), expected);
618
619 std::env::set_current_dir(original).unwrap();
620 }
621
622 #[test]
623 fn test_resolve_project_root_fallback() {
624 let result = resolve_project_root(None, None);
626 assert!(result.is_ok());
627 }
628
629 #[test]
630 fn test_detect_package_manager_pnpm() {
631 let temp = TempDir::new().unwrap();
632 File::create(temp.path().join("pnpm-lock.yaml")).unwrap();
633
634 assert_eq!(PackageManager::detect(temp.path()), PackageManager::Pnpm);
635 }
636
637 #[test]
638 fn test_detect_package_manager_yarn() {
639 let temp = TempDir::new().unwrap();
640 File::create(temp.path().join("yarn.lock")).unwrap();
641
642 assert_eq!(PackageManager::detect(temp.path()), PackageManager::Yarn);
643 }
644
645 #[test]
646 fn test_detect_package_manager_bun() {
647 let temp = TempDir::new().unwrap();
648 File::create(temp.path().join("bun.lockb")).unwrap();
649
650 assert_eq!(PackageManager::detect(temp.path()), PackageManager::Bun);
651 }
652
653 #[test]
654 fn test_detect_package_manager_npm() {
655 let temp = TempDir::new().unwrap();
656 File::create(temp.path().join("package-lock.json")).unwrap();
657
658 assert_eq!(PackageManager::detect(temp.path()), PackageManager::Npm);
659 }
660
661 #[test]
662 fn test_detect_package_manager_default_npm() {
663 let temp = TempDir::new().unwrap();
664 assert_eq!(PackageManager::detect(temp.path()), PackageManager::Npm);
667 }
668
669 #[test]
670 fn test_detect_package_manager_priority() {
671 let temp = TempDir::new().unwrap();
672 File::create(temp.path().join("pnpm-lock.yaml")).unwrap();
674 File::create(temp.path().join("yarn.lock")).unwrap();
675 File::create(temp.path().join("package-lock.json")).unwrap();
676
677 assert_eq!(PackageManager::detect(temp.path()), PackageManager::Pnpm);
678 }
679}