@@ -269,9 +269,26 @@ export async function ensureBinaryPathEntries(version: string): Promise<void> {
269269 }
270270}
271271
272- async function ensureWindowsBinJunction ( binDir : string ) : Promise < void > {
272+ function errnoCode ( error : unknown ) : string {
273+ if ( error && typeof error === "object" && "code" in error ) {
274+ return String ( ( error as { code ?: unknown } ) . code ?? "" ) ;
275+ }
276+ return "" ;
277+ }
278+
279+ /**
280+ * Ensure `shareRoot/bin` is a junction → `current`.
281+ *
282+ * Install scripts / older layouts may leave a real `bin/` directory with
283+ * `bl.exe` inside. Deleting that directory fails with EACCES while this
284+ * process is the running image — rename-away first (Windows allows that),
285+ * then create the junction. Stale `bin.migrating-*` dirs are best-effort GC.
286+ */
287+ export async function ensureWindowsBinJunction ( binDir : string ) : Promise < void > {
273288 const currentPath = getBinaryCurrentPath ( ) ;
274- const { symlink } = await import ( "node:fs/promises" ) ;
289+ const { symlink, rename } = await import ( "node:fs/promises" ) ;
290+
291+ let migratedAside : string | null = null ;
275292
276293 try {
277294 const stats = await lstat ( binDir ) ;
@@ -288,18 +305,33 @@ async function ensureWindowsBinJunction(binDir: string): Promise<void> {
288305 return ;
289306 }
290307 await unlink ( binDir ) ;
291- await symlink ( currentPath , binDir , "junction" ) ;
292- return ;
308+ } else if ( stats . isDirectory ( ) ) {
309+ // Prefer rename over rm: a running bl.exe inside bin locks delete/rm,
310+ // but rename of the directory usually succeeds on Windows.
311+ migratedAside = `${ binDir } .migrating.${ process . pid } ` ;
312+ try {
313+ await rename ( binDir , migratedAside ) ;
314+ } catch ( renameError ) {
315+ // Fallback: empty / unlocked real dirs can still be removed.
316+ try {
317+ await rm ( binDir , { recursive : true , force : true } ) ;
318+ migratedAside = null ;
319+ } catch ( rmError ) {
320+ const code = errnoCode ( renameError ) || errnoCode ( rmError ) || "EACCES" ;
321+ throw new Error (
322+ `Failed to migrate ${ binDir } to a junction pointing at current (${ code } ). ` +
323+ `Close other bl sessions and re-run update, or re-run the install script once.` ,
324+ { cause : rmError } ,
325+ ) ;
326+ }
327+ }
328+ } else {
329+ await unlink ( binDir ) . catch ( ( ) => rm ( binDir , { recursive : true , force : true } ) ) ;
293330 }
294- // Real directory from older installs: replace with junction to current.
295- // May fail if a running exe inside bin is locked — surface a clear error.
296- await rm ( binDir , { recursive : true , force : true } ) ;
297331 } catch ( error ) {
298- const code =
299- error && typeof error === "object" && "code" in error
300- ? String ( ( error as { code ?: unknown } ) . code )
301- : "" ;
332+ const code = errnoCode ( error ) ;
302333 if ( code && code !== "ENOENT" ) {
334+ if ( error instanceof Error && error . message . includes ( "Failed to migrate" ) ) throw error ;
303335 throw new Error (
304336 `Failed to migrate ${ binDir } to a junction pointing at current (${ code } ). ` +
305337 `Close other bl sessions and re-run update, or re-run the install script once.` ,
@@ -310,6 +342,11 @@ async function ensureWindowsBinJunction(binDir: string): Promise<void> {
310342
311343 await mkdir ( dirname ( binDir ) , { recursive : true } ) ;
312344 await symlink ( currentPath , binDir , "junction" ) ;
345+
346+ if ( migratedAside ) {
347+ // Best-effort: locked exes may keep the aside dir until process exit.
348+ await rm ( migratedAside , { recursive : true , force : true } ) . catch ( ( ) => { } ) ;
349+ }
313350}
314351
315352/**
0 commit comments