( packageName: string, )
| 1512 | } |
| 1513 | |
| 1514 | async function manualRemoveNpmPackage( |
| 1515 | packageName: string, |
| 1516 | ): Promise<{ success: boolean; error?: string; warning?: string }> { |
| 1517 | try { |
| 1518 | // Get npm global prefix |
| 1519 | const prefixResult = await execFileNoThrowWithCwd('npm', [ |
| 1520 | 'config', |
| 1521 | 'get', |
| 1522 | 'prefix', |
| 1523 | ]) |
| 1524 | if (prefixResult.code !== 0 || !prefixResult.stdout) { |
| 1525 | return { |
| 1526 | success: false, |
| 1527 | error: 'Failed to get npm global prefix', |
| 1528 | } |
| 1529 | } |
| 1530 | |
| 1531 | const globalPrefix = prefixResult.stdout.trim() |
| 1532 | let manuallyRemoved = false |
| 1533 | |
| 1534 | // Helper to try removing a file. unlink alone is sufficient — it throws |
| 1535 | // ENOENT if the file is missing, which the catch handles identically. |
| 1536 | // A stat() pre-check would add a syscall and a TOCTOU window where |
| 1537 | // concurrent cleanup causes a false-negative return. |
| 1538 | async function tryRemove(filePath: string, description: string) { |
| 1539 | try { |
| 1540 | await unlink(filePath) |
| 1541 | logForDebugging(`Manually removed ${description}: ${filePath}`) |
| 1542 | return true |
| 1543 | } catch { |
| 1544 | return false |
| 1545 | } |
| 1546 | } |
| 1547 | |
| 1548 | if (getPlatform().startsWith('win32')) { |
| 1549 | // Windows - only remove executables, not the package directory. |
| 1550 | // Remove current product binaries first, then legacy ones for migration. |
| 1551 | const binaryName = getBinaryName(getPlatform()) |
| 1552 | const ncodeBaseName = binaryName.replace(/\.exe$/, '') |
| 1553 | const binCmd = join(globalPrefix, `${ncodeBaseName}.cmd`) |
| 1554 | const binPs1 = join(globalPrefix, `${ncodeBaseName}.ps1`) |
| 1555 | const binExe = join(globalPrefix, ncodeBaseName) |
| 1556 | |
| 1557 | if (await tryRemove(binCmd, 'bin script')) { |
| 1558 | manuallyRemoved = true |
| 1559 | } |
| 1560 | |
| 1561 | if (await tryRemove(binPs1, 'PowerShell script')) { |
| 1562 | manuallyRemoved = true |
| 1563 | } |
| 1564 | |
| 1565 | if (await tryRemove(binExe, 'bin executable')) { |
| 1566 | manuallyRemoved = true |
| 1567 | } |
| 1568 | |
| 1569 | // Legacy cleanup: old product binary names from pre-launch internal builds. |
| 1570 | const legacyBinCmd = join(globalPrefix, 'claude.cmd') |
| 1571 | const legacyBinPs1 = join(globalPrefix, 'claude.ps1') |
no test coverage detected