import { afterEach, describe, expect, it } from 'bun:test'; import { mkdtempSync, mkdirSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'fs'; import { tmpdir } from 'os'; import { join } from 'path'; import { buildPackageManagerEnv, detectCurrentInstall, formatManualUpdateCommand, readInstalledPackageVersion, } from '../../../src/utils/package-manager-detector'; const tempDirs: string[] = []; function makeTempDir(prefix: string): string { const dir = mkdtempSync(join(tmpdir(), prefix)); tempDirs.push(dir); return dir; } function writePackage(root: string, version: string): void { mkdirSync(root, { recursive: true }); writeFileSync( join(root, 'package.json'), JSON.stringify({ name: '@kaitranntt/ccs', version }, null, 2) ); } afterEach(() => { while (tempDirs.length > 0) { const dir = tempDirs.pop(); if (dir) { rmSync(dir, { recursive: true, force: true }); } } }); describe('package-manager-detector', () => { it('detects npm installs from the current binary path and keeps the custom prefix', () => { const tempRoot = makeTempDir('ccs-install-detector-npm-'); const packageRoot = join(tempRoot, 'prefix', 'lib', 'node_modules', '@kaitranntt', 'ccs'); const scriptPath = join(packageRoot, 'dist', 'ccs.js'); writePackage(packageRoot, '7.67.0-dev.5'); const install = detectCurrentInstall(scriptPath); expect(install.manager).toBe('npm'); expect(install.prefix).toBe(join(tempRoot, 'prefix')); expect(install.packageRoot).toBe(packageRoot); expect(readInstalledPackageVersion(install)).toBe('7.67.0-dev.5'); }); it('detects bun installs from the resolved package path', () => { const tempRoot = makeTempDir('ccs-install-detector-bun-'); const packageRoot = join( tempRoot, '.bun', 'install', 'global', 'node_modules', '@kaitranntt', 'ccs' ); const scriptPath = join(packageRoot, 'dist', 'ccs.js'); writePackage(packageRoot, '7.67.0-dev.9'); const install = detectCurrentInstall(scriptPath); expect(install.manager).toBe('bun'); expect(install.prefix).toBe(join(tempRoot, '.bun')); expect(readInstalledPackageVersion(install)).toBe('7.67.0-dev.9'); }); it.if(process.platform !== 'win32')( 'detects bun installs from a POSIX symlinked ~/.bun/bin/ccs entrypoint', () => { const tempRoot = makeTempDir('ccs-install-detector-bun-symlink-'); const packageRoot = join( tempRoot, '.bun', 'install', 'global', 'node_modules', '@kaitranntt', 'ccs' ); const scriptPath = join(packageRoot, 'dist', 'ccs.js'); const symlinkPath = join(tempRoot, 'home', '.bun', 'bin', 'ccs'); writePackage(packageRoot, '7.67.0-dev.9'); mkdirSync(join(packageRoot, 'dist'), { recursive: true }); writeFileSync(scriptPath, '#!/usr/bin/env node\n'); mkdirSync(join(tempRoot, 'home', '.bun', 'bin'), { recursive: true }); symlinkSync(scriptPath, symlinkPath); const install = detectCurrentInstall(symlinkPath); const resolvedTempRoot = realpathSync(tempRoot); const resolvedScriptPath = realpathSync(scriptPath); expect(install.manager).toBe('bun'); expect(install.prefix).toBe(join(resolvedTempRoot, '.bun')); expect(install.resolvedScriptPath).toBe(resolvedScriptPath); expect(readInstalledPackageVersion(install)).toBe('7.67.0-dev.9'); } ); it('detects custom bun install roots that still use install/global/node_modules', () => { const tempRoot = makeTempDir('ccs-install-detector-custom-bun-'); const packageRoot = join( tempRoot, 'custom-bun-root', 'install', 'global', 'node_modules', '@kaitranntt', 'ccs' ); writePackage(packageRoot, '7.67.0-dev.9'); const install = detectCurrentInstall(join(packageRoot, 'dist', 'ccs.js')); expect(install.manager).toBe('bun'); expect(install.prefix).toBe(join(tempRoot, 'custom-bun-root')); }); it('detects custom yarn global layouts', () => { const tempRoot = makeTempDir('ccs-install-detector-custom-yarn-'); const packageRoot = join( tempRoot, 'custom-yarn-root', 'global', 'node_modules', '@kaitranntt', 'ccs' ); writePackage(packageRoot, '7.67.0-dev.9'); const install = detectCurrentInstall(join(packageRoot, 'dist', 'ccs.js')); expect(install.manager).toBe('yarn'); expect(install.prefix).toBe(join(tempRoot, 'custom-yarn-root')); }); it('detects custom pnpm global layouts that include a store version segment', () => { const tempRoot = makeTempDir('ccs-install-detector-custom-pnpm-'); const packageRoot = join( tempRoot, 'custom-pnpm-root', 'global', '5', '.pnpm', '@kaitranntt+ccs@7.67.0-dev.9', 'node_modules', '@kaitranntt', 'ccs' ); writePackage(packageRoot, '7.67.0-dev.9'); const install = detectCurrentInstall(join(packageRoot, 'dist', 'ccs.js')); expect(install.manager).toBe('pnpm'); expect(install.prefix).toBe(join(tempRoot, 'custom-pnpm-root')); }); it('detects pnpm global layouts without a visible .pnpm segment in the script path', () => { const tempRoot = makeTempDir('ccs-install-detector-pnpm-global-flat-'); const packageRoot = join( tempRoot, 'custom-pnpm-root', 'global', '5', 'node_modules', '@kaitranntt', 'ccs' ); writePackage(packageRoot, '7.67.0-dev.9'); const install = detectCurrentInstall(join(packageRoot, 'dist', 'ccs.js')); expect(install.manager).toBe('pnpm'); expect(install.prefix).toBe(join(tempRoot, 'custom-pnpm-root')); }); it('detects Windows npm globals without a lib directory', () => { const install = detectCurrentInstall( 'C:/Program Files/node-prefix/node_modules/@kaitranntt/ccs/dist/ccs.js' ); expect(install.manager).toBe('npm'); expect(install.prefix).toBe('C:/Program Files/node-prefix'); }); it('formats manual npm update commands with the current prefix', () => { const install = { manager: 'npm' as const, scriptPath: '/tmp/prefix/bin/ccs', resolvedScriptPath: '/tmp/prefix/lib/node_modules/@kaitranntt/ccs/dist/ccs.js', packageRoot: '/tmp/prefix/lib/node_modules/@kaitranntt/ccs', prefix: '/tmp/prefix', detectionSource: 'path' as const, }; expect(formatManualUpdateCommand('dev', install)).toBe( 'NPM_CONFIG_PREFIX=/tmp/prefix npm install -g @kaitranntt/ccs@dev' ); }); it('formats Windows-safe manual npm update commands for prefixes with spaces', () => { const install = { manager: 'npm' as const, scriptPath: 'C:/Tools/CCS/ccs.cmd', resolvedScriptPath: 'C:/Program Files/CCS/lib/node_modules/@kaitranntt/ccs/dist/ccs.js', packageRoot: 'C:/Program Files/CCS/lib/node_modules/@kaitranntt/ccs', prefix: 'C:/Program Files/CCS', detectionSource: 'path' as const, }; expect(formatManualUpdateCommand('dev', install, 'win32')).toBe( `powershell -NoProfile -Command "$env:NPM_CONFIG_PREFIX='C:/Program Files/CCS'; npm install -g @kaitranntt/ccs@dev"` ); }); it('builds manager-specific env overrides for the current install', () => { const install = { manager: 'npm' as const, scriptPath: '/tmp/prefix/bin/ccs', resolvedScriptPath: '/tmp/prefix/lib/node_modules/@kaitranntt/ccs/dist/ccs.js', packageRoot: '/tmp/prefix/lib/node_modules/@kaitranntt/ccs', prefix: '/tmp/prefix', detectionSource: 'path' as const, }; const env = buildPackageManagerEnv(install, { PATH: '/usr/bin' }); expect(env.PATH).toBe('/usr/bin'); expect(env.npm_config_prefix).toBe('/tmp/prefix'); expect(env.NPM_CONFIG_PREFIX).toBe('/tmp/prefix'); }); });