|
1 | 1 | // @ts-nocheck |
2 | 2 | import path from "node:path"; |
3 | | -import { rename, rm } from "node:fs/promises"; |
| 3 | +import { cp, rename, rm } from "node:fs/promises"; |
4 | 4 | import process from "node:process"; |
5 | 5 | import { fileURLToPath } from "node:url"; |
6 | 6 |
|
@@ -167,12 +167,39 @@ const rateLimiter = |
167 | 167 | : null; |
168 | 168 | const httpClient = createHttpClient(rateLimiter ? { rateLimiter } : {}); |
169 | 169 |
|
| 170 | +function isCrossDeviceRenameError(error: unknown) { |
| 171 | + if (!error || typeof error !== "object") { |
| 172 | + return false; |
| 173 | + } |
| 174 | + |
| 175 | + const candidate = error as { code?: unknown; message?: unknown }; |
| 176 | + if (candidate.code === "EXDEV") { |
| 177 | + return true; |
| 178 | + } |
| 179 | + |
| 180 | + if (typeof candidate.message === "string" && candidate.message.includes("EXDEV")) { |
| 181 | + return true; |
| 182 | + } |
| 183 | + |
| 184 | + return false; |
| 185 | +} |
| 186 | + |
170 | 187 | async function rotateModulesDirectory() { |
171 | 188 | const modulesExists = await fileExists(MODULES_DIR); |
172 | 189 |
|
173 | 190 | if (modulesExists) { |
174 | 191 | await rm(MODULES_TEMP_DIR, { recursive: true, force: true }); |
175 | | - await rename(MODULES_DIR, MODULES_TEMP_DIR); |
| 192 | + try { |
| 193 | + await rename(MODULES_DIR, MODULES_TEMP_DIR); |
| 194 | + } catch (error) { |
| 195 | + if (isCrossDeviceRenameError(error)) { |
| 196 | + logger.warn("Unable to rename modules directory due to cross-device link; falling back to copy/delete."); |
| 197 | + await cp(MODULES_DIR, MODULES_TEMP_DIR, { recursive: true }); |
| 198 | + await rm(MODULES_DIR, { recursive: true, force: true }); |
| 199 | + } else { |
| 200 | + throw error; |
| 201 | + } |
| 202 | + } |
176 | 203 | } else { |
177 | 204 | await ensureDirectory(MODULES_TEMP_DIR); |
178 | 205 | } |
|
0 commit comments