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