1use crate::core::types::PackageManager;
36use crate::error::{Error, Result};
37use std::collections::HashSet;
38use std::fs;
39use std::path::{Path, PathBuf};
40
41pub fn detect_package_managers(root: &Path) -> Result<Vec<PackageManager>> {
72 tracing::debug!("Detecting package managers in: {}", root.display());
73
74 let mut detections = Vec::new();
75 let mut detected_managers = HashSet::new();
76
77 detect_from_lockfiles(root, &mut detections, &mut detected_managers)?;
78 detect_from_package_json(root, &mut detections, &mut detected_managers)?;
79 detect_from_workspace_configs(root, &mut detections, &mut detected_managers)?;
80
81 Ok(prioritize_managers(detections))
82}
83
84fn detect_from_lockfiles(
86 root: &Path,
87 detections: &mut Vec<(PackageManager, u8)>,
88 detected_managers: &mut HashSet<PackageManager>,
89) -> Result<()> {
90 let lockfiles = find_lockfiles(root);
91 tracing::debug!("Found {} lockfile(s)", lockfiles.len());
92
93 for (manager, lockfile_path) in lockfiles {
94 let detected = process_lockfile(root, manager, &lockfile_path)?;
95 detections.push(detected);
96 detected_managers.insert(detected.0);
97 }
98 Ok(())
99}
100
101#[allow(clippy::cognitive_complexity)]
103fn process_lockfile(
104 root: &Path,
105 manager: PackageManager,
106 lockfile_path: &Path,
107) -> Result<(PackageManager, u8)> {
108 tracing::debug!(
109 "Processing lockfile: {} ({})",
110 lockfile_path.display(),
111 manager
112 );
113
114 let detected_manager = if matches!(manager, PackageManager::YarnClassic) {
115 detect_yarn_version(lockfile_path)?
116 } else {
117 manager
118 };
119
120 let has_valid_config = validate_workspace_config(root, detected_manager)?;
121 let confidence = calculate_confidence(true, has_valid_config);
122 tracing::debug!("Manager {} has confidence {}", detected_manager, confidence);
123
124 Ok((detected_manager, confidence))
125}
126
127fn detect_from_package_json(
129 root: &Path,
130 detections: &mut Vec<(PackageManager, u8)>,
131 detected_managers: &mut HashSet<PackageManager>,
132) -> Result<()> {
133 if let Some(manager) = detect_manager_from_package_json(root, detected_managers)? {
134 let confidence = calculate_confidence(false, true);
135 tracing::debug!(
136 "Manager {} detected via package.json (confidence {})",
137 manager,
138 confidence
139 );
140 detections.push((manager, confidence));
141 detected_managers.insert(manager);
142 }
143 Ok(())
144}
145
146fn detect_from_workspace_configs(
148 root: &Path,
149 detections: &mut Vec<(PackageManager, u8)>,
150 detected_managers: &mut HashSet<PackageManager>,
151) -> Result<()> {
152 for manager in [PackageManager::Pnpm, PackageManager::Cargo] {
153 if is_manager_detected(detected_managers, manager) {
154 continue;
155 }
156
157 if validate_workspace_config(root, manager)? {
158 let confidence = calculate_confidence(false, true);
159 tracing::debug!(
160 "Manager {} detected via config only (confidence {})",
161 manager,
162 confidence
163 );
164 detections.push((manager, confidence));
165 detected_managers.insert(manager);
166 }
167 }
168 Ok(())
169}
170
171pub fn detect_from_command(command: &str) -> Option<PackageManager> {
189 let cmd = command.split_whitespace().next().unwrap_or(command);
191
192 match cmd {
193 "cargo" => Some(PackageManager::Cargo),
194 "npm" | "npx" | "node" => Some(PackageManager::Npm),
195 "bun" | "bunx" => Some(PackageManager::Bun),
196 "pnpm" => Some(PackageManager::Pnpm),
197 "deno" => Some(PackageManager::Deno),
198 "yarn" => {
199 tracing::warn!(
200 "'yarn' command detected; defaulting to YarnClassic. For accurate version detection, use lockfile analysis via detect_yarn_version()."
201 );
202 Some(PackageManager::YarnClassic)
203 }
204 _ => None,
205 }
206}
207
208pub fn detect_with_command_hint(root: &Path, command: Option<&str>) -> Result<Vec<PackageManager>> {
233 let mut managers = detect_package_managers(root)?;
234
235 if let Some(cmd) = command
237 && let Some(hinted_manager) = detect_from_command(cmd)
238 {
239 if let Some(pos) = managers.iter().position(|m| *m == hinted_manager) {
241 if pos > 0 {
243 let manager = managers.remove(pos);
244 managers.insert(0, manager);
245 tracing::debug!("Prioritized {} based on command hint", manager);
246 }
247 }
248 }
249
250 Ok(managers)
251}
252
253fn find_lockfiles(root: &Path) -> Vec<(PackageManager, PathBuf)> {
258 let mut lockfiles = Vec::new();
259
260 let candidates = [
261 PackageManager::Npm,
262 PackageManager::Bun,
263 PackageManager::Pnpm,
264 PackageManager::YarnClassic,
265 PackageManager::Cargo,
266 PackageManager::Deno,
267 ];
268
269 for manager in candidates {
270 let lockfile_path = root.join(manager.lockfile_name());
271 if lockfile_path.exists() {
272 lockfiles.push((manager, lockfile_path));
273 }
274 }
275
276 lockfiles
277}
278
279fn detect_manager_from_package_json(
280 root: &Path,
281 detected_managers: &HashSet<PackageManager>,
282) -> Result<Option<PackageManager>> {
283 let Some(package_json) = read_package_json(root)? else {
284 return Ok(None);
285 };
286
287 let hinted_manager = package_json
288 .get("packageManager")
289 .and_then(serde_json::Value::as_str)
290 .and_then(parse_package_manager_hint);
291
292 let manager = if let Some(manager) = hinted_manager {
293 manager
294 } else {
295 if has_js_manager(detected_managers) {
296 return Ok(None);
297 }
298 PackageManager::Npm
299 };
300
301 if is_manager_detected(detected_managers, manager) {
302 return Ok(None);
303 }
304
305 Ok(Some(manager))
306}
307
308fn read_package_json(root: &Path) -> Result<Option<serde_json::Value>> {
309 let path = root.join("package.json");
310 if !path.exists() {
311 return Ok(None);
312 }
313
314 let content = fs::read_to_string(&path).map_err(|e| Error::Io {
315 source: e,
316 path: Some(path.clone()),
317 operation: "reading workspace config".to_string(),
318 })?;
319
320 let parsed = serde_json::from_str::<serde_json::Value>(&content).map_err(|e| {
321 Error::InvalidWorkspaceConfig {
322 path: path.clone(),
323 message: format!("Invalid JSON: {e}"),
324 }
325 })?;
326
327 Ok(Some(parsed))
328}
329
330fn parse_package_manager_hint(hint: &str) -> Option<PackageManager> {
331 let trimmed = hint.trim();
332 if trimmed.is_empty() {
333 return None;
334 }
335
336 let (manager_name, version_part) = match trimmed.split_once('@') {
337 Some((name, version)) if !name.is_empty() => (name, version),
338 _ => (trimmed, ""),
339 };
340
341 let normalized_name = manager_name.trim().to_ascii_lowercase();
342
343 match normalized_name.as_str() {
344 "npm" => Some(PackageManager::Npm),
345 "bun" => Some(PackageManager::Bun),
346 "yarn" => {
347 let major = parse_major_version(version_part);
348 match major {
349 Some(value) if value < 2 => Some(PackageManager::YarnClassic),
350 _ => Some(PackageManager::YarnModern),
351 }
352 }
353 _ => None,
354 }
355}
356
357fn parse_major_version(input: &str) -> Option<u64> {
358 let trimmed = input.trim().trim_start_matches(['v', 'V']);
359 let digits: String = trimmed.chars().take_while(char::is_ascii_digit).collect();
360
361 if digits.is_empty() {
362 return None;
363 }
364
365 digits.parse::<u64>().ok()
366}
367
368fn is_manager_detected(
369 detected_managers: &HashSet<PackageManager>,
370 manager: PackageManager,
371) -> bool {
372 match manager {
373 PackageManager::YarnClassic | PackageManager::YarnModern => {
374 detected_managers.contains(&PackageManager::YarnClassic)
375 || detected_managers.contains(&PackageManager::YarnModern)
376 }
377 _ => detected_managers.contains(&manager),
378 }
379}
380
381fn has_js_manager(detected_managers: &HashSet<PackageManager>) -> bool {
382 detected_managers.contains(&PackageManager::Npm)
383 || detected_managers.contains(&PackageManager::Bun)
384 || detected_managers.contains(&PackageManager::Pnpm)
385 || detected_managers.contains(&PackageManager::YarnClassic)
386 || detected_managers.contains(&PackageManager::YarnModern)
387 || detected_managers.contains(&PackageManager::Deno)
388}
389
390fn validate_workspace_config(root: &Path, manager: PackageManager) -> Result<bool> {
395 let config_path = root.join(manager.workspace_config_name());
396
397 if !config_path.exists() {
399 return Ok(false);
400 }
401
402 let content = fs::read_to_string(&config_path).map_err(|e| Error::Io {
404 source: e,
405 path: Some(config_path.clone()),
406 operation: "reading workspace config".to_string(),
407 })?;
408
409 match manager {
411 PackageManager::Npm
412 | PackageManager::Bun
413 | PackageManager::YarnClassic
414 | PackageManager::YarnModern
415 | PackageManager::Deno => {
416 serde_json::from_str::<serde_json::Value>(&content).map_err(|e| {
418 Error::InvalidWorkspaceConfig {
419 path: config_path,
420 message: format!("Invalid JSON: {e}"),
421 }
422 })?;
423 Ok(true)
424 }
425 PackageManager::Cargo => {
426 toml::from_str::<toml::Value>(&content).map_err(|e| Error::InvalidWorkspaceConfig {
428 path: config_path,
429 message: format!("Invalid TOML: {e}"),
430 })?;
431 Ok(true)
432 }
433 PackageManager::Pnpm => {
434 serde_yaml::from_str::<serde_yaml::Value>(&content).map_err(|e| {
436 Error::InvalidWorkspaceConfig {
437 path: config_path,
438 message: format!("Invalid YAML: {e}"),
439 }
440 })?;
441 Ok(true)
442 }
443 }
444}
445
446fn detect_yarn_version(lockfile_path: &Path) -> Result<PackageManager> {
452 let content = fs::read_to_string(lockfile_path).map_err(|e| Error::Io {
453 source: e,
454 path: Some(lockfile_path.to_path_buf()),
455 operation: "reading yarn.lock".to_string(),
456 })?;
457
458 let first_lines: String = content.lines().take(5).collect::<Vec<_>>().join("\n");
460
461 if first_lines.contains("__metadata:") {
462 Ok(PackageManager::YarnModern)
463 } else if first_lines.contains("# yarn lockfile v1")
464 || first_lines.contains("# THIS IS AN AUTOGENERATED FILE")
465 {
466 Ok(PackageManager::YarnClassic)
467 } else {
468 tracing::warn!(
470 "Could not determine Yarn version from lockfile format, defaulting to Classic"
471 );
472 Ok(PackageManager::YarnClassic)
473 }
474}
475
476const fn calculate_confidence(has_lockfile: bool, has_valid_config: bool) -> u8 {
484 match (has_lockfile, has_valid_config) {
485 (true, true) => 100,
486 (true, false) => 75,
487 (false, true) => 50,
488 (false, false) => 0,
489 }
490}
491
492fn prioritize_managers(detections: Vec<(PackageManager, u8)>) -> Vec<PackageManager> {
497 let mut sorted = detections;
498
499 sorted.sort_by(|(m1, c1), (m2, c2)| {
501 match c2.cmp(c1) {
503 std::cmp::Ordering::Equal => {
504 manager_priority(*m1).cmp(&manager_priority(*m2))
506 }
507 other => other,
508 }
509 });
510
511 sorted.into_iter().map(|(m, _)| m).collect()
512}
513
514const fn manager_priority(manager: PackageManager) -> u8 {
518 match manager {
519 PackageManager::Cargo => 0,
520 PackageManager::Deno => 1,
521 PackageManager::Bun => 2,
522 PackageManager::Pnpm => 3,
523 PackageManager::YarnModern => 4,
524 PackageManager::YarnClassic => 5,
525 PackageManager::Npm => 6,
526 }
527}
528
529#[cfg(test)]
530#[allow(clippy::match_same_arms)]
531mod tests {
532 use super::*;
533 use tempfile::TempDir;
534
535 fn create_test_workspace() -> TempDir {
537 TempDir::new().expect("Failed to create temp dir")
538 }
539
540 fn create_lockfile(dir: &Path, manager: PackageManager) -> PathBuf {
542 let lockfile_path = dir.join(manager.lockfile_name());
543 let content = match manager {
544 PackageManager::Npm => r#"{"lockfileVersion": 2}"#,
545 PackageManager::Bun => "binary content",
546 PackageManager::Pnpm => "lockfileVersion: '6.0'",
547 PackageManager::YarnClassic => "# yarn lockfile v1\n",
548 PackageManager::YarnModern => "__metadata:\n version: 6\n",
549 PackageManager::Cargo => "[root]\n",
550 PackageManager::Deno => r#"{"version": "3"}"#,
551 };
552 fs::write(&lockfile_path, content).expect("Failed to write lockfile");
553 lockfile_path
554 }
555
556 fn create_workspace_config(dir: &Path, manager: PackageManager) -> PathBuf {
558 let config_path = dir.join(manager.workspace_config_name());
559 let content = match manager {
560 PackageManager::Npm | PackageManager::Bun => {
561 r#"{"name": "test", "workspaces": ["packages/*"]}"#
562 }
563 PackageManager::YarnClassic | PackageManager::YarnModern => {
564 r#"{"name": "test", "workspaces": ["packages/*"]}"#
565 }
566 PackageManager::Pnpm => "packages:\n - 'packages/*'\n",
567 PackageManager::Cargo => "[workspace]\nmembers = [\"crates/*\"]\n",
568 PackageManager::Deno => r#"{"name": "test", "workspace": ["packages/*"]}"#,
569 };
570 fs::write(&config_path, content).expect("Failed to write config");
571 config_path
572 }
573
574 #[test]
575 fn test_calculate_confidence() {
576 assert_eq!(calculate_confidence(true, true), 100);
577 assert_eq!(calculate_confidence(true, false), 75);
578 assert_eq!(calculate_confidence(false, true), 50);
579 assert_eq!(calculate_confidence(false, false), 0);
580 }
581
582 #[test]
583 fn test_prioritize_managers() {
584 let detections = vec![
585 (PackageManager::Npm, 75),
586 (PackageManager::Cargo, 100),
587 (PackageManager::Bun, 75),
588 ];
589
590 let result = prioritize_managers(detections);
591
592 assert_eq!(result[0], PackageManager::Cargo); assert_eq!(result[1], PackageManager::Bun); assert_eq!(result[2], PackageManager::Npm);
595 }
596
597 #[test]
598 fn test_prioritize_managers_equal_confidence() {
599 let detections = vec![
600 (PackageManager::Npm, 75),
601 (PackageManager::Bun, 75),
602 (PackageManager::Cargo, 75),
603 ];
604
605 let result = prioritize_managers(detections);
606
607 assert_eq!(result[0], PackageManager::Cargo);
609 assert_eq!(result[1], PackageManager::Bun);
610 assert_eq!(result[2], PackageManager::Npm);
611 }
612
613 #[test]
614 fn test_detect_from_command() {
615 assert_eq!(detect_from_command("cargo"), Some(PackageManager::Cargo));
616 assert_eq!(detect_from_command("npm"), Some(PackageManager::Npm));
617 assert_eq!(detect_from_command("npx"), Some(PackageManager::Npm));
618 assert_eq!(detect_from_command("bun"), Some(PackageManager::Bun));
619 assert_eq!(detect_from_command("bunx"), Some(PackageManager::Bun));
620 assert_eq!(detect_from_command("pnpm"), Some(PackageManager::Pnpm));
621 assert_eq!(detect_from_command("deno"), Some(PackageManager::Deno));
622 assert_eq!(
623 detect_from_command("yarn"),
624 Some(PackageManager::YarnClassic)
625 );
626 assert_eq!(detect_from_command("node"), Some(PackageManager::Npm));
627 }
628
629 #[test]
630 fn test_detect_from_command_with_args() {
631 assert_eq!(
632 detect_from_command("cargo build"),
633 Some(PackageManager::Cargo)
634 );
635 assert_eq!(
636 detect_from_command("npm install"),
637 Some(PackageManager::Npm)
638 );
639 assert_eq!(
640 detect_from_command("bun run test"),
641 Some(PackageManager::Bun)
642 );
643 assert_eq!(
644 detect_from_command("bunx eslint"),
645 Some(PackageManager::Bun)
646 );
647 assert_eq!(
648 detect_from_command("npx prisma generate"),
649 Some(PackageManager::Npm)
650 );
651 }
652
653 #[test]
654 fn test_detect_from_command_unknown() {
655 assert_eq!(detect_from_command("unknown"), None);
656 assert_eq!(detect_from_command("make"), None);
657 assert_eq!(detect_from_command("python"), None);
658 }
659
660 #[test]
661 fn test_detect_yarn_classic() {
662 let temp_dir = create_test_workspace();
663 let lockfile_path = temp_dir.path().join("yarn.lock");
664
665 let classic_content = r#"# yarn lockfile v1
666# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
667
668package@^1.0.0:
669 version "1.0.0"
670"#;
671 fs::write(&lockfile_path, classic_content).unwrap();
672
673 let result = detect_yarn_version(&lockfile_path).unwrap();
674 assert_eq!(result, PackageManager::YarnClassic);
675 }
676
677 #[test]
678 fn test_detect_yarn_modern() {
679 let temp_dir = create_test_workspace();
680 let lockfile_path = temp_dir.path().join("yarn.lock");
681
682 let modern_content = r#"__metadata:
683 version: 6
684 cacheKey: 8
685
686"package@npm:^1.0.0":
687 version: 1.0.0
688"#;
689 fs::write(&lockfile_path, modern_content).unwrap();
690
691 let result = detect_yarn_version(&lockfile_path).unwrap();
692 assert_eq!(result, PackageManager::YarnModern);
693 }
694
695 #[test]
696 fn test_find_lockfiles_cargo() {
697 let temp_dir = create_test_workspace();
698 create_lockfile(temp_dir.path(), PackageManager::Cargo);
699
700 let result = find_lockfiles(temp_dir.path());
701
702 assert_eq!(result.len(), 1);
703 assert_eq!(result[0].0, PackageManager::Cargo);
704 }
705
706 #[test]
707 fn test_find_lockfiles_npm() {
708 let temp_dir = create_test_workspace();
709 create_lockfile(temp_dir.path(), PackageManager::Npm);
710
711 let result = find_lockfiles(temp_dir.path());
712
713 assert_eq!(result.len(), 1);
714 assert_eq!(result[0].0, PackageManager::Npm);
715 }
716
717 #[test]
718 fn test_find_lockfiles_multiple() {
719 let temp_dir = create_test_workspace();
720 create_lockfile(temp_dir.path(), PackageManager::Cargo);
721 create_lockfile(temp_dir.path(), PackageManager::Npm);
722
723 let result = find_lockfiles(temp_dir.path());
724
725 assert_eq!(result.len(), 2);
726 let managers: Vec<_> = result.iter().map(|(m, _)| *m).collect();
727 assert!(managers.contains(&PackageManager::Cargo));
728 assert!(managers.contains(&PackageManager::Npm));
729 }
730
731 #[test]
732 fn test_find_lockfiles_none() {
733 let temp_dir = create_test_workspace();
734
735 let result = find_lockfiles(temp_dir.path());
736
737 assert_eq!(result.len(), 0);
738 }
739
740 #[test]
741 fn test_find_lockfiles_bun_text() {
742 let temp_dir = create_test_workspace();
743
744 fs::write(temp_dir.path().join("bun.lock"), "text").unwrap();
745
746 let result = find_lockfiles(temp_dir.path());
747
748 assert_eq!(result.len(), 1);
750 assert_eq!(result[0].0, PackageManager::Bun);
751 assert_eq!(result[0].1.file_name().unwrap(), "bun.lock");
752 }
753
754 #[test]
755 fn test_validate_workspace_config_cargo() {
756 let temp_dir = create_test_workspace();
757 create_workspace_config(temp_dir.path(), PackageManager::Cargo);
758
759 let result = validate_workspace_config(temp_dir.path(), PackageManager::Cargo).unwrap();
760 assert!(result);
761 }
762
763 #[test]
764 fn test_validate_workspace_config_package_json() {
765 let temp_dir = create_test_workspace();
766 create_workspace_config(temp_dir.path(), PackageManager::Npm);
767
768 let result = validate_workspace_config(temp_dir.path(), PackageManager::Npm).unwrap();
769 assert!(result);
770 }
771
772 #[test]
773 fn test_validate_workspace_config_pnpm() {
774 let temp_dir = create_test_workspace();
775 create_workspace_config(temp_dir.path(), PackageManager::Pnpm);
776
777 let result = validate_workspace_config(temp_dir.path(), PackageManager::Pnpm).unwrap();
778 assert!(result);
779 }
780
781 #[test]
782 fn test_validate_workspace_config_invalid_json() {
783 let temp_dir = create_test_workspace();
784 let config_path = temp_dir.path().join("package.json");
785 fs::write(&config_path, "{ invalid json }").unwrap();
786
787 let result = validate_workspace_config(temp_dir.path(), PackageManager::Npm);
788 assert!(result.is_err());
789 assert!(matches!(result, Err(Error::InvalidWorkspaceConfig { .. })));
790 }
791
792 #[test]
793 fn test_validate_workspace_config_invalid_toml() {
794 let temp_dir = create_test_workspace();
795 let config_path = temp_dir.path().join("Cargo.toml");
796 fs::write(&config_path, "[invalid toml").unwrap();
797
798 let result = validate_workspace_config(temp_dir.path(), PackageManager::Cargo);
799 assert!(result.is_err());
800 assert!(matches!(result, Err(Error::InvalidWorkspaceConfig { .. })));
801 }
802
803 #[test]
804 fn test_validate_workspace_config_invalid_yaml() {
805 let temp_dir = create_test_workspace();
806 let config_path = temp_dir.path().join("pnpm-workspace.yaml");
807 fs::write(&config_path, "invalid: yaml: content:").unwrap();
808
809 let result = validate_workspace_config(temp_dir.path(), PackageManager::Pnpm);
810 assert!(result.is_err());
811 assert!(matches!(result, Err(Error::InvalidWorkspaceConfig { .. })));
812 }
813
814 #[test]
815 fn test_validate_workspace_config_missing() {
816 let temp_dir = create_test_workspace();
817
818 let result = validate_workspace_config(temp_dir.path(), PackageManager::Npm).unwrap();
819 assert!(!result); }
821
822 #[test]
823 fn test_detect_package_managers_cargo() {
824 let temp_dir = create_test_workspace();
825 create_lockfile(temp_dir.path(), PackageManager::Cargo);
826 create_workspace_config(temp_dir.path(), PackageManager::Cargo);
827
828 let result = detect_package_managers(temp_dir.path()).unwrap();
829
830 assert_eq!(result.len(), 1);
831 assert_eq!(result[0], PackageManager::Cargo);
832 }
833
834 #[test]
835 fn test_detect_package_managers_npm() {
836 let temp_dir = create_test_workspace();
837 create_lockfile(temp_dir.path(), PackageManager::Npm);
838 create_workspace_config(temp_dir.path(), PackageManager::Npm);
839
840 let result = detect_package_managers(temp_dir.path()).unwrap();
841
842 assert_eq!(result, vec![PackageManager::Npm]);
843 }
844
845 #[test]
846 fn test_detect_package_managers_multi() {
847 let temp_dir = create_test_workspace();
848
849 create_lockfile(temp_dir.path(), PackageManager::Cargo);
851 create_workspace_config(temp_dir.path(), PackageManager::Cargo);
852
853 fs::write(
855 temp_dir.path().join("package.json"),
856 r#"{"name": "test", "workspaces": ["packages/*"]}"#,
857 )
858 .unwrap();
859 create_lockfile(temp_dir.path(), PackageManager::Bun);
860
861 let result = detect_package_managers(temp_dir.path()).unwrap();
862
863 assert!(result.len() >= 2);
866 assert_eq!(result[0], PackageManager::Cargo);
868 assert_eq!(result[1], PackageManager::Bun);
869 }
870
871 #[test]
872 fn test_detect_package_managers_lockfile_only() {
873 let temp_dir = create_test_workspace();
874 create_lockfile(temp_dir.path(), PackageManager::Cargo);
875 let result = detect_package_managers(temp_dir.path()).unwrap();
878
879 assert_eq!(result.len(), 1);
880 assert_eq!(result[0], PackageManager::Cargo);
881 }
882
883 #[test]
884 fn test_detect_package_managers_config_only() {
885 let temp_dir = create_test_workspace();
886 create_workspace_config(temp_dir.path(), PackageManager::Cargo);
887 let result = detect_package_managers(temp_dir.path()).unwrap();
890
891 assert_eq!(result.len(), 1);
892 assert_eq!(result[0], PackageManager::Cargo);
893 }
894
895 #[test]
896 fn test_detect_package_managers_empty_dir() {
897 let temp_dir = create_test_workspace();
898
899 let result = detect_package_managers(temp_dir.path()).unwrap();
900
901 assert_eq!(result.len(), 0);
902 }
903
904 #[test]
905 fn test_detect_with_command_hint() {
906 let temp_dir = create_test_workspace();
907
908 create_lockfile(temp_dir.path(), PackageManager::Cargo);
910 create_workspace_config(temp_dir.path(), PackageManager::Cargo);
911
912 fs::write(
913 temp_dir.path().join("package.json"),
914 r#"{"name": "test", "workspaces": ["packages/*"]}"#,
915 )
916 .unwrap();
917 create_lockfile(temp_dir.path(), PackageManager::Bun);
918
919 let result = detect_package_managers(temp_dir.path()).unwrap();
921 assert_eq!(result[0], PackageManager::Cargo);
922
923 let result = detect_with_command_hint(temp_dir.path(), Some("bun run test")).unwrap();
925 assert_eq!(result[0], PackageManager::Bun);
926 assert_eq!(result[1], PackageManager::Cargo);
927 }
928
929 #[test]
930 fn test_detect_with_command_hint_not_detected() {
931 let temp_dir = create_test_workspace();
932 create_lockfile(temp_dir.path(), PackageManager::Cargo);
933 create_workspace_config(temp_dir.path(), PackageManager::Cargo);
934
935 let result = detect_with_command_hint(temp_dir.path(), Some("npm install")).unwrap();
937 assert_eq!(result.len(), 1);
938 assert_eq!(result[0], PackageManager::Cargo);
939 }
940
941 #[test]
942 fn test_detect_with_no_command_hint() {
943 let temp_dir = create_test_workspace();
944 create_lockfile(temp_dir.path(), PackageManager::Cargo);
945
946 let result = detect_with_command_hint(temp_dir.path(), None).unwrap();
947 assert_eq!(result.len(), 1);
948 assert_eq!(result[0], PackageManager::Cargo);
949 }
950
951 #[test]
952 fn test_yarn_version_detection_in_detect_package_managers() {
953 let temp_dir = create_test_workspace();
954
955 let lockfile_path = temp_dir.path().join("yarn.lock");
957 fs::write(&lockfile_path, "__metadata:\n version: 6\n").unwrap();
958 create_workspace_config(temp_dir.path(), PackageManager::YarnModern);
959
960 let result = detect_package_managers(temp_dir.path()).unwrap();
961
962 assert_eq!(result, vec![PackageManager::YarnModern]);
963 }
964
965 #[test]
966 fn test_package_json_config_only_defaults_to_npm() {
967 let temp_dir = create_test_workspace();
968
969 fs::write(
970 temp_dir.path().join("package.json"),
971 r#"{"name":"example"}"#,
972 )
973 .unwrap();
974
975 let result = detect_package_managers(temp_dir.path()).unwrap();
976
977 assert_eq!(result, vec![PackageManager::Npm]);
978 }
979
980 #[test]
981 fn test_package_json_package_manager_hint_yarn_classic() {
982 let temp_dir = create_test_workspace();
983
984 fs::write(
985 temp_dir.path().join("package.json"),
986 r#"{"name":"example","packageManager":"yarn@1.22.0"}"#,
987 )
988 .unwrap();
989
990 let result = detect_package_managers(temp_dir.path()).unwrap();
991
992 assert_eq!(result, vec![PackageManager::YarnClassic]);
993 }
994
995 #[test]
996 fn test_package_json_package_manager_hint_yarn_modern() {
997 let temp_dir = create_test_workspace();
998
999 fs::write(
1000 temp_dir.path().join("package.json"),
1001 r#"{"name":"example","packageManager":"yarn@3.5.1"}"#,
1002 )
1003 .unwrap();
1004
1005 let result = detect_package_managers(temp_dir.path()).unwrap();
1006
1007 assert_eq!(result, vec![PackageManager::YarnModern]);
1008 }
1009
1010 #[test]
1011 fn test_package_json_package_manager_hint_bun() {
1012 let temp_dir = create_test_workspace();
1013
1014 fs::write(
1015 temp_dir.path().join("package.json"),
1016 r#"{"name":"example","packageManager":"bun@1.0.0"}"#,
1017 )
1018 .unwrap();
1019
1020 let result = detect_package_managers(temp_dir.path()).unwrap();
1021
1022 assert_eq!(result, vec![PackageManager::Bun]);
1023 }
1024
1025 #[test]
1026 fn test_yarn_modern_lockfile_does_not_duplicate_classic() {
1027 let temp_dir = create_test_workspace();
1028 create_lockfile(temp_dir.path(), PackageManager::YarnModern);
1029 fs::write(
1030 temp_dir.path().join("package.json"),
1031 r#"{"name":"example"}"#,
1032 )
1033 .unwrap();
1034
1035 let result = detect_package_managers(temp_dir.path()).unwrap();
1036
1037 assert_eq!(result, vec![PackageManager::YarnModern]);
1038 }
1039
1040 #[test]
1041 fn test_manager_priority() {
1042 assert!(manager_priority(PackageManager::Cargo) < manager_priority(PackageManager::Deno));
1043 assert!(manager_priority(PackageManager::Deno) < manager_priority(PackageManager::Bun));
1044 assert!(manager_priority(PackageManager::Bun) < manager_priority(PackageManager::Pnpm));
1045 assert!(
1046 manager_priority(PackageManager::Pnpm) < manager_priority(PackageManager::YarnModern)
1047 );
1048 assert!(
1049 manager_priority(PackageManager::YarnModern)
1050 < manager_priority(PackageManager::YarnClassic)
1051 );
1052 assert!(
1053 manager_priority(PackageManager::YarnClassic) < manager_priority(PackageManager::Npm)
1054 );
1055 }
1056}