Skip to content

Adds Windows support#322

Open
arcanis wants to merge 30 commits into
mainfrom
mael/windows
Open

Adds Windows support#322
arcanis wants to merge 30 commits into
mainfrom
mael/windows

Conversation

@arcanis

@arcanis arcanis commented Jul 16, 2026

Copy link
Copy Markdown
Member

Summary

  • make filesystem paths, permissions, exit statuses, and daemon process management portable to Windows
  • honor winLinkType with real NTFS junction support in the node-modules and pnpm linkers
  • build, package, install, and publish the x86_64-pc-windows-msvc executables
  • run the acceptance test matrix on windows-latest

Validation

  • cargo build -r
  • cargo check --workspace --all-targets
  • cargo xwin check --workspace --all-targets --target x86_64-pc-windows-msvc
  • workflow YAML parsing and bash -n install-script.sh
  • full integration run: 1,599 passed, 50 skipped, 6 failed; the failures were three existing timeout-sensitive tests, two lazy-install timestamp assertions, and the existing TypeScript plugin fixture expectation
  • focused Windows-sensitive integration run: node-modules, pnpm, switch daemon, and 197 other tests passed; one script case timed out and the same TypeScript plugin fixture failed
  • Rust library tests passed except the three existing PathIterator expectations that disagree with the implementation and its doc examples about emitting the empty relative root

Note

High Risk
Large cross-cutting changes to path handling, linking, installs, and process management on a new platform; mistakes could affect resolution, node_modules layout, or daemon behavior on all OSes where shared code paths changed.

Overview
Adds first-class Windows (x86_64-pc-windows-msvc) support across build, release, install, and runtime.

CI & distribution: Build/test matrices include windows-latest with .exe artifacts; Windows release zips use PowerShell; npm publish maps win32 and .exe binaries; install-script.sh detects MSYS/Git Bash and installs yarn.exe.

Core portability: zpm_utils::Path gains portable ↔ native conversion (drive letters, UNC), to_native_string / to_path_buf, Windows symlinks and junctions (junction crate), ACL-based 0o600 files, and cross-drive-safe relative_to_if_same_root. Process helpers move to zpm-utils (alive/terminate, PATHEXT / .cmd resolution, portable exit codes). Script execution and subprocess spawning use native paths and Windows shims.

Linking & sync: winLinkType (symlinks vs junctions) is honored in node-modules and pnpm linkers; zpm-sync can create junctions and compares link targets correctly on Windows.

Node & daemons: Builtin @yarnpkg/node adds win-x64 / win-arm64 zip variants; lockfile reuse re-resolves when variant coverage is missing for configured systems. Switch daemon lifecycle uses taskkill on Windows; MSVC build sets an 8 MiB stack for yarn-bin.

Behavior fixes: CRLF in JSON whitespace; parent tasks inherit subtask failure exit codes; git diff path normalization for Windows; acceptance tests extended for Windows node dist and architectures.

Reviewed by Cursor Bugbot for commit 51c1b87. Bugbot is set up for automated code reviews on this repo. Configure here.

@netlify

netlify Bot commented Jul 16, 2026

Copy link
Copy Markdown

Deploy Preview for yarn-v6 ready!

Name Link
🔨 Latest commit 51c1b87
🔍 Latest deploy log https://app.netlify.com/projects/yarn-v6/deploys/6a59f902beed7400084868cb
😎 Deploy Preview https://deploy-preview-322--yarn-v6.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Windows zip keeps nested paths
    • Modified Windows archive step to copy files to a temporary directory first, then archive from there to create a flat zip structure matching Linux/macOS builds.

Create PR

Or push these changes by commenting:

@cursor push 070e900f78
Preview (070e900f78)
diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml
--- a/.github/workflows/releases.yml
+++ b/.github/workflows/releases.yml
@@ -89,11 +89,12 @@
         if: runner.os == 'Windows'
         shell: pwsh
         run: |
-          Compress-Archive -Path `
-            target/${{matrix.target}}/release-lto-nodebug/yarn-bin${{matrix.ext}}, `
-            target/${{matrix.target}}/release-lto-nodebug/yarn${{matrix.ext}}, `
-            target/${{matrix.target}}/release-lto-nodebug/LICENSE.md `
-            -DestinationPath yarn-${{matrix.target}}.zip
+          $tmpDir = New-Item -ItemType Directory -Path "tmp-archive-${{matrix.target}}"
+          Copy-Item -Path "target/${{matrix.target}}/release-lto-nodebug/yarn-bin${{matrix.ext}}" -Destination $tmpDir
+          Copy-Item -Path "target/${{matrix.target}}/release-lto-nodebug/yarn${{matrix.ext}}" -Destination $tmpDir
+          Copy-Item -Path "target/${{matrix.target}}/release-lto-nodebug/LICENSE.md" -Destination $tmpDir
+          Compress-Archive -Path "$tmpDir/*" -DestinationPath "yarn-${{matrix.target}}.zip"
+          Remove-Item -Recurse -Force $tmpDir
 
       - name: Upload artifacts
         uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7

You can send follow-ups to the cloud agent here.

Comment thread .github/workflows/releases.yml Outdated
@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown

⏱️ Benchmark Results

gatsby install-full-cold

Metric Base Head Difference
Mean 4.490s 4.496s +0.15% ⚠️
Median 4.492s 4.495s +0.06% ⚠️
Min 4.338s 4.361s
Max 4.611s 4.695s
Std Dev 0.054s 0.077s
📊 Raw benchmark data (gatsby install-full-cold)

Base times: 4.451s, 4.462s, 4.415s, 4.436s, 4.594s, 4.509s, 4.508s, 4.470s, 4.492s, 4.492s, 4.505s, 4.450s, 4.611s, 4.533s, 4.526s, 4.521s, 4.447s, 4.528s, 4.442s, 4.535s, 4.521s, 4.338s, 4.493s, 4.479s, 4.456s, 4.548s, 4.490s, 4.493s, 4.431s, 4.509s

Head times: 4.379s, 4.522s, 4.555s, 4.445s, 4.524s, 4.420s, 4.462s, 4.499s, 4.525s, 4.498s, 4.504s, 4.361s, 4.443s, 4.400s, 4.462s, 4.471s, 4.445s, 4.479s, 4.695s, 4.529s, 4.451s, 4.492s, 4.423s, 4.458s, 4.669s, 4.606s, 4.623s, 4.526s, 4.516s, 4.509s


gatsby install-cache-only

Metric Base Head Difference
Mean 1.313s 1.292s -1.58% ✅
Median 1.314s 1.290s -1.83% ✅
Min 1.276s 1.269s
Max 1.380s 1.356s
Std Dev 0.020s 0.019s
📊 Raw benchmark data (gatsby install-cache-only)

Base times: 1.310s, 1.329s, 1.284s, 1.325s, 1.324s, 1.303s, 1.326s, 1.315s, 1.295s, 1.314s, 1.318s, 1.325s, 1.310s, 1.321s, 1.321s, 1.313s, 1.291s, 1.298s, 1.326s, 1.320s, 1.300s, 1.380s, 1.335s, 1.276s, 1.314s, 1.278s, 1.306s, 1.330s, 1.305s, 1.304s

Head times: 1.323s, 1.320s, 1.271s, 1.294s, 1.291s, 1.280s, 1.294s, 1.291s, 1.283s, 1.284s, 1.290s, 1.274s, 1.301s, 1.289s, 1.282s, 1.303s, 1.286s, 1.274s, 1.291s, 1.356s, 1.293s, 1.269s, 1.296s, 1.274s, 1.292s, 1.288s, 1.279s, 1.285s, 1.288s, 1.331s


gatsby install-cache-and-lock (warm, with lockfile)

Metric Base Head Difference
Mean 0.357s 0.355s -0.80% ✅
Median 0.355s 0.353s -0.61% ✅
Min 0.350s 0.347s
Max 0.374s 0.379s
Std Dev 0.006s 0.007s
📊 Raw benchmark data (gatsby install-cache-and-lock (warm, with lockfile))

Base times: 0.356s, 0.359s, 0.364s, 0.356s, 0.353s, 0.351s, 0.355s, 0.354s, 0.354s, 0.352s, 0.350s, 0.355s, 0.356s, 0.354s, 0.350s, 0.350s, 0.353s, 0.361s, 0.355s, 0.354s, 0.355s, 0.352s, 0.374s, 0.367s, 0.360s, 0.359s, 0.363s, 0.363s, 0.370s, 0.368s

Head times: 0.351s, 0.352s, 0.354s, 0.353s, 0.352s, 0.354s, 0.354s, 0.379s, 0.358s, 0.351s, 0.353s, 0.354s, 0.351s, 0.358s, 0.358s, 0.348s, 0.351s, 0.350s, 0.376s, 0.358s, 0.351s, 0.353s, 0.350s, 0.353s, 0.351s, 0.358s, 0.347s, 0.356s, 0.350s, 0.353s

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Windows daemon kill ignores children
    • Replaced single-process TerminateProcess with taskkill /T to terminate the entire process tree and added wait loop to verify termination before returning.

Create PR

Or push these changes by commenting:

@cursor push 4e17f2c392
Preview (4e17f2c392)
diff --git a/packages/zpm-switch/src/daemons.rs b/packages/zpm-switch/src/daemons.rs
--- a/packages/zpm-switch/src/daemons.rs
+++ b/packages/zpm-switch/src/daemons.rs
@@ -165,8 +165,27 @@
 
     #[cfg(windows)]
     {
-        // On Windows, just use TerminateProcess (no graceful shutdown)
-        kill_process(pid)
+        // On Windows, use taskkill /T to terminate the process tree
+        // /T terminates the process and all its child processes
+        // /F forces termination without graceful shutdown
+        let result = std::process::Command::new("taskkill")
+            .args(["/PID", &pid.to_string(), "/T", "/F"])
+            .output();
+        
+        if result.is_err() {
+            return false;
+        }
+        
+        // Wait for the process to actually terminate
+        for _ in 0..10 {
+            std::thread::sleep(std::time::Duration::from_millis(100));
+            if !is_process_alive(pid) {
+                return true;
+            }
+        }
+        
+        // Return false if we couldn't verify termination
+        !is_process_alive(pid)
     }
 
     #[cfg(not(any(unix, windows)))]

You can send follow-ups to the cloud agent here.

Comment thread packages/zpm-switch/src/daemons.rs

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Switch sibling misses exe suffix
    • Added std::env::consts::EXE_SUFFIX to the sibling yarn path lookup so Windows correctly finds yarn.exe.

Create PR

Or push these changes by commenting:

@cursor push 5e2c164043
Preview (5e2c164043)
diff --git a/packages/zpm/src/daemon/client.rs b/packages/zpm/src/daemon/client.rs
--- a/packages/zpm/src/daemon/client.rs
+++ b/packages/zpm/src/daemon/client.rs
@@ -493,7 +493,7 @@
                 let current_dir
                     = current_exe.dirname().ok_or(())?;
                 let sibling_switch
-                    = current_dir.with_join_str("yarn");
+                    = current_dir.with_join_str(&format!("yarn{}", std::env::consts::EXE_SUFFIX));
 
                 if sibling_switch.fs_exists() {
                     Ok(sibling_switch.to_native_string())

You can send follow-ups to the cloud agent here.

Comment thread packages/zpm/src/daemon/client.rs

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Junction sync skips up-to-date check
    • Added directory check for junctions since Rust reports them as directories rather than symlinks, enabling proper freshness detection.

Create PR

Or push these changes by commenting:

@cursor push ea996118a8
Preview (ea996118a8)
diff --git a/packages/zpm-sync/src/lib.rs b/packages/zpm-sync/src/lib.rs
--- a/packages/zpm-sync/src/lib.rs
+++ b/packages/zpm-sync/src/lib.rs
@@ -320,7 +320,7 @@
 
             SyncNode::Symlink {target_path} => {
                 let symlink_target
-                    = metadata.is_symlink()
+                    = (metadata.is_symlink() || (self.link_type == LinkType::Junction && metadata.is_dir()))
                         .then(|| path.fs_read_link())
                         .transpose()?;

You can send follow-ups to the cloud agent here.

Comment thread packages/zpm-sync/src/lib.rs

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Pnpm junction replace uses remove_file
    • Replaced fs_rm_file() with fs_rm() at all three link creation sites to properly handle both files and directory junctions on Windows.

Create PR

Or push these changes by commenting:

@cursor push bd18209e5c
Preview (bd18209e5c)
diff --git a/packages/zpm/src/linker/pnpm.rs b/packages/zpm/src/linker/pnpm.rs
--- a/packages/zpm/src/linker/pnpm.rs
+++ b/packages/zpm/src/linker/pnpm.rs
@@ -207,7 +207,7 @@
             .relative_to(&link_abs_dirname);
 
         let link_path = link_abs_path
-            .fs_rm_file()
+            .fs_rm()
             .ok_missing()?
             .unwrap_or(&link_abs_path)
             .fs_create_parent()?;
@@ -253,7 +253,7 @@
                 .relative_to(&link_abs_dirname);
 
         let link_path = link_abs_path
-            .fs_rm_file()
+            .fs_rm()
             .ok_missing()?
             .unwrap_or(&link_abs_path)
             .fs_create_parent()?;
@@ -326,7 +326,7 @@
                     .relative_to(&link_abs_dirname);
 
             let link_path = link_abs_path
-                .fs_rm_file()
+                .fs_rm()
                 .ok_missing()?
                 .unwrap_or(&link_abs_path)
                 .fs_create_parent()?;

You can send follow-ups to the cloud agent here.

Comment thread packages/zpm/src/linker/pnpm.rs

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Daemon registry lacks Windows ACLs
    • Implemented Windows ACL support in fs_set_mode(0o600) using SDDL to restrict daemon registry files to owner-only access, preventing other local users from reading daemon credentials including authToken.

Create PR

Or push these changes by commenting:

@cursor push 72c8ba6ba4
Preview (72c8ba6ba4)
diff --git a/Cargo.toml b/Cargo.toml
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -117,7 +117,7 @@
 serde_json = { version = "1.0.145", features = ["preserve_order"] }
 serde = { version = "1.0.207", features = ["derive"] }
 wax = { git = "https://github.com/arcanis/wax.git" }
-windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_System_Threading"] }
+windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_System_Threading", "Win32_Security", "Win32_Security_Authorization"] }
 winnow = "0.7"
 url = "2.5.7"
 urlencoding = "2.1.3"

diff --git a/packages/zpm-utils/src/path.rs b/packages/zpm-utils/src/path.rs
--- a/packages/zpm-utils/src/path.rs
+++ b/packages/zpm-utils/src/path.rs
@@ -542,7 +542,77 @@
             self.fs_set_permissions(std::fs::Permissions::from_mode(mode))?;
         }
 
-        #[cfg(not(unix))]
+        #[cfg(windows)]
+        {
+            // On Windows, apply ACLs for mode 0o600 (owner-only read/write)
+            if mode == 0o600 {
+                use std::ffi::OsStr;
+                use std::os::windows::ffi::OsStrExt;
+                use windows_sys::Win32::Security::Authorization::{
+                    SetNamedSecurityInfoW, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION,
+                    PROTECTED_DACL_SECURITY_INFORMATION,
+                };
+                use windows_sys::Win32::Security::{
+                    ConvertStringSecurityDescriptorToSecurityDescriptorW,
+                    PSECURITY_DESCRIPTOR, SDDL_REVISION_1,
+                };
+                use windows_sys::Win32::System::Memory::LocalFree;
+                use windows_sys::core::PWSTR;
+
+                // SDDL string for owner-only full access, protected from inheritance
+                // D:P(A;;FA;;;OW) means:
+                // D: - DACL
+                // P - Protected (don't inherit from parent)
+                // A - Allow ACE
+                // ;; - No object/inherited object type
+                // FA - FILE_ALL_ACCESS
+                // ;;; - No flags/object/inherited object
+                // OW - Owner Rights SID
+                let sddl = "D:P(A;;FA;;;OW)\0";
+                let sddl_wide: Vec<u16> = OsStr::new(sddl).encode_wide().collect();
+
+                unsafe {
+                    let mut sd: PSECURITY_DESCRIPTOR = std::ptr::null_mut();
+                    let result = ConvertStringSecurityDescriptorToSecurityDescriptorW(
+                        sddl_wide.as_ptr(),
+                        SDDL_REVISION_1,
+                        &mut sd,
+                        std::ptr::null_mut(),
+                    );
+
+                    if result == 0 {
+                        return Err(std::io::Error::last_os_error().into());
+                    }
+
+                    // Convert path to wide string
+                    let path_wide: Vec<u16> = OsStr::new(&self.path)
+                        .encode_wide()
+                        .chain(std::iter::once(0))
+                        .collect();
+
+                    // Apply the security descriptor to the file
+                    let set_result = SetNamedSecurityInfoW(
+                        path_wide.as_ptr() as PWSTR,
+                        SE_FILE_OBJECT,
+                        DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION,
+                        std::ptr::null_mut(),
+                        std::ptr::null_mut(),
+                        std::ptr::null_mut(),
+                        std::ptr::null_mut(),
+                    );
+
+                    // Free the security descriptor
+                    LocalFree(sd as isize);
+
+                    if set_result != 0 {
+                        return Err(std::io::Error::from_raw_os_error(set_result as i32).into());
+                    }
+                }
+            }
+            // For other modes on Windows, we currently don't have an implementation
+        }
+
+        #[cfg(not(any(unix, windows)))]
         let _ = mode;
 
         Ok(self)

You can send follow-ups to the cloud agent here.

Comment thread packages/zpm-switch/src/daemons.rs

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Install script misses ARM64 Windows
    • Added pattern matching for ARM64 Windows environments (MINGW/MSYS/Cygwin with ARM64/aarch64) to use x86_64-pc-windows-msvc target via emulation.

Create PR

Or push these changes by commenting:

@cursor push 039a87be1d
Preview (039a87be1d)
diff --git a/install-script.sh b/install-script.sh
--- a/install-script.sh
+++ b/install-script.sh
@@ -77,6 +77,9 @@
 'MINGW64_NT'*' x86_64' | 'MSYS_NT'*' x86_64' | 'CYGWIN_NT'*' x86_64')
   target=x86_64-pc-windows-msvc
   ;;
+'MINGW64_NT'*' ARM64' | 'MINGW64_NT'*' aarch64' | 'MSYS_NT'*' ARM64' | 'MSYS_NT'*' aarch64' | 'CYGWIN_NT'*' ARM64' | 'CYGWIN_NT'*' aarch64')
+  target=x86_64-pc-windows-msvc
+  ;;
 'Linux aarch64' | 'Linux arm64')
   target=aarch64-unknown-linux-musl
   ;;

You can send follow-ups to the cloud agent here.

Comment thread install-script.sh

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: UNC extended prefix lost
    • Modified to_portable_path and from_portable_path to preserve the \?\UNC\ extended prefix using /unc/?/UNC/ portable representation.
  • ✅ Fixed: Junction sync deletes real dirs
    • Restructured symlink check logic to only treat actual junctions (where fs_read_link succeeds) as link candidates, preventing deletion of regular directories.

Create PR

Or push these changes by commenting:

@cursor push 828dde4cd8
Preview (828dde4cd8)
diff --git a/packages/zpm-sync/src/lib.rs b/packages/zpm-sync/src/lib.rs
--- a/packages/zpm-sync/src/lib.rs
+++ b/packages/zpm-sync/src/lib.rs
@@ -319,16 +319,17 @@
             },
 
             SyncNode::Symlink {target_path} => {
-                let is_link_candidate
-                    = metadata.is_symlink() || (self.link_type == LinkType::Junction && metadata.is_dir());
-
                 let symlink_target
-                    = if is_link_candidate {
+                    = if metadata.is_symlink() {
                         match path.fs_read_link() {
                             Ok(target) => Some(target),
-                            Err(_) if self.link_type == LinkType::Junction && metadata.is_dir() && !metadata.is_symlink() => None,
                             Err(error) => return Err(error.into()),
                         }
+                    } else if self.link_type == LinkType::Junction && metadata.is_dir() {
+                        match path.fs_read_link() {
+                            Ok(target) => Some(target),
+                            Err(_) => None,
+                        }
                     } else {
                         None
                     };

diff --git a/packages/zpm-utils/src/path.rs b/packages/zpm-utils/src/path.rs
--- a/packages/zpm-utils/src/path.rs
+++ b/packages/zpm-utils/src/path.rs
@@ -11,7 +11,7 @@
     let value = value.replace('\\', "/");
 
     if let Some(value) = value.strip_prefix("//?/UNC/") {
-        format!("/unc/{}", value)
+        format!("/unc/?/UNC/{}", value)
     } else if value.starts_with("//?/")
         && value.as_bytes().get(5) == Some(&b':')
         && value.as_bytes().get(4).map_or(false, u8::is_ascii_alphabetic)
@@ -38,6 +38,10 @@
         value[1..].replace('/', "\\")
     } else if let Some(value) = value.strip_prefix("/unc/.dot/") {
         format!("\\\\.\\{}", value.replace('/', "\\"))
+    } else if let Some(value) = value.strip_prefix("/unc/?/UNC/") {
+        format!("\\\\?\\UNC\\{}", value.replace('/', "\\"))
+    } else if let Some(value) = value.strip_prefix("/unc/?/") {
+        format!("\\\\?\\{}", value.replace('/', "\\"))
     } else if let Some(value) = value.strip_prefix("/unc/") {
         format!("\\\\{}", value.replace('/', "\\"))
     } else {
@@ -1452,12 +1456,13 @@
         assert_eq!(to_portable_path(r"\\server\share\project"), "/unc/server/share/project");
         assert_eq!(to_portable_path(r"\\.\pipe\yarn"), "/unc/.dot/pipe/yarn");
         assert_eq!(to_portable_path(r"\\?\C:\work\project"), "/C:/work/project");
-        assert_eq!(to_portable_path(r"\\?\UNC\server\share\project"), "/unc/server/share/project");
+        assert_eq!(to_portable_path(r"\\?\UNC\server\share\project"), "/unc/?/UNC/server/share/project");
 
         assert_eq!(from_portable_path("/C:/work/project"), r"C:\work\project");
         assert_eq!(from_portable_path("/unc/server/share/project"), r"\\server\share\project");
         assert_eq!(from_portable_path("/unc/.dot/pipe/yarn"), r"\\.\pipe\yarn");
         assert_eq!(from_portable_path("/unc/?/C:/work/project"), r"\\?\C:\work\project");
+        assert_eq!(from_portable_path("/unc/?/UNC/server/share/project"), r"\\?\UNC\server\share\project");
     }
 
     #[test]

You can send follow-ups to the cloud agent here.

Comment thread packages/zpm-utils/src/path.rs
Comment thread packages/zpm-sync/src/lib.rs

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Junction check errors on missing targets
    • Modified junction canonicalization logic to return false on failures instead of propagating errors, restoring self-healing behavior for broken junctions.

Create PR

Or push these changes by commenting:

@cursor push 4a5820ac01
Preview (4a5820ac01)
diff --git a/packages/zpm-sync/src/lib.rs b/packages/zpm-sync/src/lib.rs
--- a/packages/zpm-sync/src/lib.rs
+++ b/packages/zpm-sync/src/lib.rs
@@ -340,7 +340,10 @@
                                 path.dirname().unwrap_or_default().with_join(target_path)
                             };
 
-                            actual_target.fs_canonicalize()? == expected_target.fs_canonicalize()?
+                            match (actual_target.fs_canonicalize(), expected_target.fs_canonicalize()) {
+                                (Ok(actual_canonical), Ok(expected_canonical)) => actual_canonical == expected_canonical,
+                                _ => false,
+                            }
                         },
                         (_, Some(actual_target)) => actual_target == *target_path,
                         (_, None) => false,

You can send follow-ups to the cloud agent here.

Comment thread packages/zpm-sync/src/lib.rs Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Junction mode relative link churn
    • Applied canonical path comparison to relative symlink targets in junction mode by converting them to absolute paths before canonicalization, preventing unnecessary link recreation.

Create PR

Or push these changes by commenting:

@cursor push 701221b217
Preview (701221b217)
diff --git a/packages/zpm-sync/src/lib.rs b/packages/zpm-sync/src/lib.rs
--- a/packages/zpm-sync/src/lib.rs
+++ b/packages/zpm-sync/src/lib.rs
@@ -333,14 +333,20 @@
 
                 let is_symlink_up_to_date
                     = match (self.link_type, symlink_target) {
-                        (LinkType::Junction, Some(actual_target)) if actual_target.is_absolute() => {
+                        (LinkType::Junction, Some(actual_target)) => {
                             let expected_target = if target_path.is_absolute() {
                                 target_path.clone()
                             } else {
                                 path.dirname().unwrap_or_default().with_join(target_path)
                             };
 
-                            match (actual_target.fs_canonicalize(), expected_target.fs_canonicalize()) {
+                            let actual_full = if actual_target.is_absolute() {
+                                actual_target
+                            } else {
+                                path.dirname().unwrap_or_default().with_join(&actual_target)
+                            };
+
+                            match (actual_full.fs_canonicalize(), expected_target.fs_canonicalize()) {
                                 (Ok(actual_canonical), Ok(expected_canonical)) => actual_canonical == expected_canonical,
                                 _ => false,
                             }

You can send follow-ups to the cloud agent here.

Comment thread packages/zpm-sync/src/lib.rs
Comment thread packages/zpm-utils/src/process.rs

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Portal paths no longer canonical
    • Changed all workspace loading and portal resolution calls to use canonical_destination instead of destination_path for consistent path normalization.
  • ✅ Fixed: Cross-drive relative portals broken
    • Replaced relative_to with relative_to_if_same_root in add_resolution to handle cross-drive paths correctly on Windows.

Create PR

Or push these changes by commenting:

@cursor push 301db3f46c
Preview (301db3f46c)
diff --git a/packages/zpm/src/commands/link.rs b/packages/zpm/src/commands/link.rs
--- a/packages/zpm/src/commands/link.rs
+++ b/packages/zpm/src/commands/link.rs
@@ -79,7 +79,7 @@
             }
 
             let target_workspace
-                = Workspace::from_root_path(&destination_path)?;
+                = Workspace::from_root_path(&canonical_destination)?;
 
             if self.all {
                 let child_workspaces
@@ -87,7 +87,7 @@
 
                 if let Some(name) = &target_workspace.manifest.name {
                     if self.private || !target_workspace.manifest.private.unwrap_or(false) {
-                        self.add_resolution(&mut document, name, &destination_path, root_path)?;
+                        self.add_resolution(&mut document, name, &canonical_destination, root_path)?;
                     }
                 }
 
@@ -105,9 +105,9 @@
             } else {
                 let name
                     = target_workspace.manifest.name.as_ref()
-                        .ok_or_else(|| Error::LinkedPackageMissingName(destination_path.clone()))?;
+                        .ok_or_else(|| Error::LinkedPackageMissingName(canonical_destination.clone()))?;
 
-                self.add_resolution(&mut document, name, &destination_path, root_path)?;
+                self.add_resolution(&mut document, name, &canonical_destination, root_path)?;
             }
         }
 
@@ -118,7 +118,7 @@
 
     fn add_resolution(&self, document: &mut JsonDocument, name: &Ident, workspace_path: &Path, root_path: &Path) -> Result<(), Error> {
         let portal_path = if self.relative {
-            workspace_path.relative_to(root_path)
+            workspace_path.relative_to_if_same_root(root_path)
         } else {
             workspace_path.clone()
         };

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 7f9c5d5. Configure here.

Comment thread packages/zpm/src/commands/link.rs
Comment thread packages/zpm/src/commands/link.rs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant