33use std:: {
44 collections:: HashSet ,
55 io:: { Read , Write } ,
6+ path:: Path ,
67 process:: Stdio ,
78} ;
89
@@ -269,6 +270,10 @@ pub async fn uninstall(package_name: &str, dry_run: bool) -> Result<(), Error> {
269270
270271/// Parse package spec into name and optional version.
271272fn parse_package_spec ( spec : & str ) -> ( String , Option < String > ) {
273+ if is_local_path_spec ( spec) {
274+ return ( resolve_local_package_name ( spec) . unwrap_or_else ( || spec. to_string ( ) ) , None ) ;
275+ }
276+
272277 // Handle scoped packages: @scope/name@version
273278 if spec. starts_with ( '@' ) {
274279 // Find the second @ for version
@@ -287,6 +292,27 @@ fn parse_package_spec(spec: &str) -> (String, Option<String>) {
287292 ( spec. to_string ( ) , None )
288293}
289294
295+ fn is_local_path_spec ( spec : & str ) -> bool {
296+ spec == "."
297+ || spec == ".."
298+ || spec. starts_with ( "./" )
299+ || spec. starts_with ( "../" )
300+ || spec. starts_with ( ".\\ " )
301+ || spec. starts_with ( "..\\ " )
302+ || Path :: new ( spec) . is_absolute ( )
303+ }
304+
305+ fn resolve_local_package_name ( spec : & str ) -> Option < String > {
306+ let package_dir = if Path :: new ( spec) . is_absolute ( ) {
307+ Path :: new ( spec) . to_path_buf ( )
308+ } else {
309+ std:: env:: current_dir ( ) . ok ( ) ?. join ( spec)
310+ } ;
311+ let package_json = std:: fs:: read_to_string ( package_dir. join ( "package.json" ) ) . ok ( ) ?;
312+ let json: serde_json:: Value = serde_json:: from_str ( & package_json) . ok ( ) ?;
313+ json. get ( "name" ) . and_then ( |value| value. as_str ( ) ) . map ( str:: to_string)
314+ }
315+
290316/// Binary info extracted from package.json.
291317struct BinaryInfo {
292318 /// Binary name (the command users will run)
@@ -470,6 +496,22 @@ async fn remove_package_shim(
470496mod tests {
471497 use super :: * ;
472498
499+ struct CurrentDirGuard ( std:: path:: PathBuf ) ;
500+
501+ impl CurrentDirGuard {
502+ fn change_to ( path : & std:: path:: Path ) -> Self {
503+ let previous = std:: env:: current_dir ( ) . unwrap ( ) ;
504+ std:: env:: set_current_dir ( path) . unwrap ( ) ;
505+ Self ( previous)
506+ }
507+ }
508+
509+ impl Drop for CurrentDirGuard {
510+ fn drop ( & mut self ) {
511+ std:: env:: set_current_dir ( & self . 0 ) . unwrap ( ) ;
512+ }
513+ }
514+
473515 /// RAII guard that sets `VP_TRAMPOLINE_PATH` to a fake binary on creation
474516 /// and clears it on drop. Ensures cleanup even on test panics.
475517 #[ cfg( windows) ]
@@ -729,6 +771,27 @@ mod tests {
729771 assert_eq ! ( version, Some ( "20.0.0" . to_string( ) ) ) ;
730772 }
731773
774+ #[ test]
775+ #[ serial_test:: serial]
776+ fn test_parse_package_spec_local_path_uses_package_name ( ) {
777+ use tempfile:: TempDir ;
778+
779+ let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
780+ let _cwd_guard = CurrentDirGuard :: change_to ( temp_dir. path ( ) ) ;
781+ let package_dir = temp_dir. path ( ) . join ( "fixture-pkg" ) ;
782+
783+ std:: fs:: create_dir_all ( & package_dir) . unwrap ( ) ;
784+ std:: fs:: write (
785+ package_dir. join ( "package.json" ) ,
786+ r#"{ "name": "resolved-local-package", "version": "1.0.0" }"# ,
787+ )
788+ . unwrap ( ) ;
789+
790+ let ( name, version) = parse_package_spec ( "./fixture-pkg" ) ;
791+ assert_eq ! ( name, "resolved-local-package" ) ;
792+ assert_eq ! ( version, None ) ;
793+ }
794+
732795 #[ test]
733796 fn test_is_javascript_binary_with_js_extension ( ) {
734797 use tempfile:: TempDir ;
0 commit comments