( packageName: string, )
| 1523 | } |
| 1524 | |
| 1525 | async function manualRemoveNpmPackage( |
| 1526 | packageName: string, |
| 1527 | ): Promise<{ success: boolean; error?: string; warning?: string }> { |
| 1528 | try { |
| 1529 | // Get npm global prefix |
| 1530 | const prefixResult = await execFileNoThrowWithCwd('npm', [ |
| 1531 | 'config', |
| 1532 | 'get', |
| 1533 | 'prefix', |
| 1534 | ]) |
| 1535 | if (prefixResult.code !== 0 || !prefixResult.stdout) { |
| 1536 | return { |
| 1537 | success: false, |
| 1538 | error: 'Failed to get npm global prefix', |
| 1539 | } |
| 1540 | } |
| 1541 | |
| 1542 | const globalPrefix = prefixResult.stdout.trim() |
| 1543 | let manuallyRemoved = false |
| 1544 | |
| 1545 | // Helper to try removing a file. unlink alone is sufficient — it throws |
| 1546 | // ENOENT if the file is missing, which the catch handles identically. |
| 1547 | // A stat() pre-check would add a syscall and a TOCTOU window where |
| 1548 | // concurrent cleanup causes a false-negative return. |
| 1549 | async function tryRemove(filePath: string, description: string) { |
| 1550 | try { |
| 1551 | await unlink(filePath) |
| 1552 | logForDebugging(`Manually removed ${description}: ${filePath}`) |
| 1553 | return true |
| 1554 | } catch { |
| 1555 | return false |
| 1556 | } |
| 1557 | } |
| 1558 | |
| 1559 | if (getPlatform().startsWith('win32')) { |
| 1560 | // Windows - only remove executables, not the package directory |
| 1561 | const binCmd = join(globalPrefix, 'claude.cmd') |
| 1562 | const binPs1 = join(globalPrefix, 'claude.ps1') |
| 1563 | const binExe = join(globalPrefix, 'claude') |
| 1564 | |
| 1565 | if (await tryRemove(binCmd, 'bin script')) { |
| 1566 | manuallyRemoved = true |
| 1567 | } |
| 1568 | |
| 1569 | if (await tryRemove(binPs1, 'PowerShell script')) { |
| 1570 | manuallyRemoved = true |
| 1571 | } |
| 1572 | |
| 1573 | if (await tryRemove(binExe, 'bin executable')) { |
| 1574 | manuallyRemoved = true |
| 1575 | } |
| 1576 | } else { |
| 1577 | // Unix/Mac - only remove symlink, not the package directory |
| 1578 | const binSymlink = join(globalPrefix, 'bin', 'claude') |
| 1579 | |
| 1580 | if (await tryRemove(binSymlink, 'bin symlink')) { |
| 1581 | manuallyRemoved = true |
| 1582 | } |
no test coverage detected