fix: improve web self-update package manager detection
- Detect update package manager from install ownership across npm, pnpm, yarn, and bun - Fix web self-update restart command so the server comes back cleanly after install - Add clearer update logs with package manager choice, commands, and restart context
This commit is contained in:
@@ -61,7 +61,7 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
|||||||
const {
|
const {
|
||||||
checkForUpdates,
|
checkForUpdates,
|
||||||
getUpdateCommand,
|
getUpdateCommand,
|
||||||
detectPackageManager,
|
detectPackageManagerDetails,
|
||||||
} = await import('../package-manager.js');
|
} = await import('../package-manager.js');
|
||||||
|
|
||||||
const updateInfo = await checkForUpdates();
|
const updateInfo = await checkForUpdates();
|
||||||
@@ -69,7 +69,8 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
|||||||
return res.status(400).json({ error: 'No update available' });
|
return res.status(400).json({ error: 'No update available' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const pm = detectPackageManager();
|
const pmDetails = detectPackageManagerDetails();
|
||||||
|
const pm = pmDetails.packageManager;
|
||||||
const updateCmd = getUpdateCommand(pm);
|
const updateCmd = getUpdateCommand(pm);
|
||||||
const isContainer =
|
const isContainer =
|
||||||
fs.existsSync('/.dockerenv') ||
|
fs.existsSync('/.dockerenv') ||
|
||||||
@@ -119,17 +120,16 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
|||||||
return `"${stringValue.replace(/"/g, '""')}"`;
|
return `"${stringValue.replace(/"/g, '""')}"`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const cliPath = path.resolve(__dirname, '..', '..', 'bin', 'cli.js');
|
const cliPath = path.resolve(__dirname, '..', 'bin', 'cli.js');
|
||||||
const restartParts = [
|
const restartParts = [
|
||||||
isWindows ? quoteCmd(process.execPath) : quotePosix(process.execPath),
|
isWindows ? quoteCmd(process.execPath) : quotePosix(process.execPath),
|
||||||
isWindows ? quoteCmd(cliPath) : quotePosix(cliPath),
|
isWindows ? quoteCmd(cliPath) : quotePosix(cliPath),
|
||||||
'serve',
|
'serve',
|
||||||
'--port',
|
'--port',
|
||||||
String(storedOptions.port),
|
String(storedOptions.port),
|
||||||
'--daemon',
|
|
||||||
];
|
];
|
||||||
let restartCmdPrimary = restartParts.join(' ');
|
let restartCmdPrimary = restartParts.join(' ');
|
||||||
let restartCmdFallback = `openchamber serve --port ${storedOptions.port} --daemon`;
|
let restartCmdFallback = `openchamber serve --port ${storedOptions.port}`;
|
||||||
if (storedOptions.uiPassword) {
|
if (storedOptions.uiPassword) {
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
const escapedPw = storedOptions.uiPassword.replace(/"/g, '""');
|
const escapedPw = storedOptions.uiPassword.replace(/"/g, '""');
|
||||||
@@ -142,6 +142,22 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const restartCmd = `(${restartCmdPrimary}) || (${restartCmdFallback})`;
|
const restartCmd = `(${restartCmdPrimary}) || (${restartCmdFallback})`;
|
||||||
|
const updateLogPath = path.join(openchamberDataDir, 'update-install.log');
|
||||||
|
const logPreamble = [
|
||||||
|
'',
|
||||||
|
`=== OpenChamber update ${new Date().toISOString()} ===`,
|
||||||
|
`currentVersion=${updateInfo.currentVersion || 'unknown'}`,
|
||||||
|
`targetVersion=${updateInfo.version || 'unknown'}`,
|
||||||
|
`packageManager=${pm}`,
|
||||||
|
`packageManagerReason=${pmDetails.reason || 'unknown'}`,
|
||||||
|
`packageManagerCommand=${pmDetails.packageManagerCommand || 'unknown'}`,
|
||||||
|
`packagePath=${pmDetails.packagePath || 'unknown'}`,
|
||||||
|
`globalNodeModulesRoot=${pmDetails.globalNodeModulesRoot || 'unknown'}`,
|
||||||
|
`mode=${isContainer ? 'container' : 'restart'}`,
|
||||||
|
`updateCommand=${updateCmd}`,
|
||||||
|
`restartCommand=${restartCmd}`,
|
||||||
|
`logPath=${updateLogPath}`,
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -151,14 +167,16 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
|||||||
autoRestart: true,
|
autoRestart: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
console.log(`\nInstalling update using ${pm}...`);
|
console.log(`\nInstalling update using ${pm}...`);
|
||||||
console.log(`Running: ${updateCmd}`);
|
console.log(`Running: ${updateCmd}`);
|
||||||
|
console.log(logPreamble);
|
||||||
|
|
||||||
const shell = isWindows ? (process.env.ComSpec || 'cmd.exe') : 'sh';
|
const shell = isWindows ? (process.env.ComSpec || 'cmd.exe') : 'sh';
|
||||||
const shellFlag = isWindows ? '/c' : '-c';
|
const shellFlag = isWindows ? '/c' : '-c';
|
||||||
const script = isWindows
|
const script = isWindows
|
||||||
? `
|
? `
|
||||||
|
echo ${quoteCmd(logPreamble)}
|
||||||
timeout /t 2 /nobreak >nul
|
timeout /t 2 /nobreak >nul
|
||||||
${updateCmd}
|
${updateCmd}
|
||||||
if %ERRORLEVEL% EQU 0 (
|
if %ERRORLEVEL% EQU 0 (
|
||||||
@@ -168,8 +186,9 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
|||||||
echo Update failed
|
echo Update failed
|
||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
`
|
`
|
||||||
: `
|
: `
|
||||||
|
printf '%s\n' ${quotePosix(logPreamble)}
|
||||||
sleep 2
|
sleep 2
|
||||||
${updateCmd}
|
${updateCmd}
|
||||||
if [ $? -eq 0 ]; then
|
if [ $? -eq 0 ]; then
|
||||||
@@ -181,7 +200,6 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
|||||||
fi
|
fi
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const updateLogPath = path.join(openchamberDataDir, 'update-install.log');
|
|
||||||
let logFd = null;
|
let logFd = null;
|
||||||
try {
|
try {
|
||||||
fs.mkdirSync(path.dirname(updateLogPath), { recursive: true });
|
fs.mkdirSync(path.dirname(updateLogPath), { recursive: true });
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const __filename = fileURLToPath(import.meta.url);
|
|||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
const PACKAGE_NAME = '@openchamber/web';
|
const PACKAGE_NAME = '@openchamber/web';
|
||||||
|
const PACKAGE_PATH_SEGMENTS = PACKAGE_NAME.split('/');
|
||||||
const NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}`;
|
const NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}`;
|
||||||
const CHANGELOG_URL = 'https://raw.githubusercontent.com/btriapitsyn/openchamber/main/CHANGELOG.md';
|
const CHANGELOG_URL = 'https://raw.githubusercontent.com/btriapitsyn/openchamber/main/CHANGELOG.md';
|
||||||
let cachedDetectedPm = null;
|
let cachedDetectedPm = null;
|
||||||
@@ -131,17 +132,209 @@ async function checkForUpdatesFromApi(currentVersion, options = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function normalizePathForComparison(filePath) {
|
||||||
* Detect which package manager was used to install this package.
|
if (!filePath || typeof filePath !== 'string') return null;
|
||||||
* Strategy:
|
const normalized = path.normalize(path.resolve(filePath));
|
||||||
* 1. Check npm_config_user_agent (set during npm/pnpm/yarn/bun install)
|
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
|
||||||
* 2. Check npm_execpath for PM binary path
|
}
|
||||||
* 3. Analyze package location path for PM-specific patterns
|
|
||||||
* 4. Fall back to npm
|
function getComparablePaths(filePath) {
|
||||||
*/
|
const paths = new Set();
|
||||||
export function detectPackageManager() {
|
const normalized = normalizePathForComparison(filePath);
|
||||||
|
if (normalized) {
|
||||||
|
paths.add(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const realPath = fs.realpathSync.native ? fs.realpathSync.native(filePath) : fs.realpathSync(filePath);
|
||||||
|
const normalizedRealPath = normalizePathForComparison(realPath);
|
||||||
|
if (normalizedRealPath) {
|
||||||
|
paths.add(normalizedRealPath);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pathSetContains(a, b) {
|
||||||
|
for (const value of a) {
|
||||||
|
if (b.has(value)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentPackagePath() {
|
||||||
|
return path.resolve(__dirname, '..', '..');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPackagePathForGlobalRoot(rootPath) {
|
||||||
|
if (!rootPath) return null;
|
||||||
|
return path.join(rootPath, ...PACKAGE_PATH_SEGMENTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUniquePaths(paths) {
|
||||||
|
const seen = new Set();
|
||||||
|
const result = [];
|
||||||
|
for (const value of paths) {
|
||||||
|
const normalized = normalizePathForComparison(value);
|
||||||
|
if (!normalized || seen.has(normalized)) continue;
|
||||||
|
seen.add(normalized);
|
||||||
|
result.push(path.resolve(value));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCommandOutput(command, args) {
|
||||||
|
try {
|
||||||
|
const result = spawnSync(command, args, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
timeout: 10000,
|
||||||
|
...getSpawnSyncBaseOptions(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.status !== 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stdout = result.stdout.trim();
|
||||||
|
return stdout || null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGlobalBinDirs(pm) {
|
||||||
|
const pmCommand = resolvePackageManagerCommand(pm);
|
||||||
|
if (!isCommandAvailable(pmCommand)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const dirs = [];
|
||||||
|
switch (pm) {
|
||||||
|
case 'pnpm': {
|
||||||
|
const pnpmBin = getCommandOutput(pmCommand, ['bin', '-g']);
|
||||||
|
if (pnpmBin) dirs.push(pnpmBin);
|
||||||
|
const pnpmPrefix = getCommandOutput(pmCommand, ['prefix', '-g']);
|
||||||
|
if (pnpmPrefix) dirs.push(process.platform === 'win32' ? pnpmPrefix : path.join(pnpmPrefix, 'bin'));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'yarn': {
|
||||||
|
const yarnBin = getCommandOutput(pmCommand, ['global', 'bin']);
|
||||||
|
if (yarnBin) dirs.push(yarnBin);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'bun': {
|
||||||
|
const bunBin = getCommandOutput(pmCommand, ['pm', 'bin', '-g']);
|
||||||
|
if (bunBin) dirs.push(bunBin);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
const npmPrefix = getCommandOutput(pmCommand, ['prefix', '-g']);
|
||||||
|
if (npmPrefix) dirs.push(process.platform === 'win32' ? npmPrefix : path.join(npmPrefix, 'bin'));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return getUniquePaths(dirs);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGlobalNodeModulesRoots(pm) {
|
||||||
|
try {
|
||||||
|
const pmCommand = resolvePackageManagerCommand(pm);
|
||||||
|
if (!isCommandAvailable(pmCommand)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const roots = [];
|
||||||
|
|
||||||
|
switch (pm) {
|
||||||
|
case 'pnpm': {
|
||||||
|
const pnpmRoot = getCommandOutput(pmCommand, ['root', '-g']);
|
||||||
|
if (pnpmRoot) roots.push(pnpmRoot);
|
||||||
|
const pnpmPrefix = getCommandOutput(pmCommand, ['prefix', '-g']);
|
||||||
|
if (pnpmPrefix) roots.push(process.platform === 'win32' ? path.join(pnpmPrefix, 'node_modules') : path.join(pnpmPrefix, 'lib', 'node_modules'));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'yarn': {
|
||||||
|
const yarnDir = getCommandOutput(pmCommand, ['global', 'dir']);
|
||||||
|
if (yarnDir) roots.push(path.join(yarnDir, 'node_modules'));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'bun': {
|
||||||
|
const bunBinDir = getCommandOutput(pmCommand, ['pm', 'bin', '-g']);
|
||||||
|
if (bunBinDir) {
|
||||||
|
roots.push(path.resolve(bunBinDir, '..', 'install', 'global', 'node_modules'));
|
||||||
|
roots.push(path.resolve(bunBinDir, '..', '..', 'node_modules'));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
const npmRoot = getCommandOutput(pmCommand, ['root', '-g']);
|
||||||
|
if (npmRoot) roots.push(npmRoot);
|
||||||
|
const npmPrefix = getCommandOutput(pmCommand, ['prefix', '-g']);
|
||||||
|
if (npmPrefix) roots.push(process.platform === 'win32' ? path.join(npmPrefix, 'node_modules') : path.join(npmPrefix, 'lib', 'node_modules'));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return getUniquePaths(roots);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOwnedPackagePathsFromGlobalBins(pm) {
|
||||||
|
const packagePaths = [];
|
||||||
|
for (const binDir of getGlobalBinDirs(pm)) {
|
||||||
|
const binaryName = process.platform === 'win32' ? 'openchamber.cmd' : 'openchamber';
|
||||||
|
const binaryPath = path.join(binDir, binaryName);
|
||||||
|
if (!fs.existsSync(binaryPath)) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const realBinaryPath = fs.realpathSync.native ? fs.realpathSync.native(binaryPath) : fs.realpathSync(binaryPath);
|
||||||
|
packagePaths.push(path.resolve(realBinaryPath, '..', '..'));
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return getUniquePaths(packagePaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectPackageManagerFromCurrentInstallPath() {
|
||||||
|
return detectPackageManagerFromInstallPath(getCurrentPackagePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
function packageManagerOwnsCurrentInstall(pm) {
|
||||||
|
const currentPackagePaths = getComparablePaths(getCurrentPackagePath());
|
||||||
|
const candidatePackagePaths = [
|
||||||
|
...getGlobalNodeModulesRoots(pm).map(getPackagePathForGlobalRoot),
|
||||||
|
...getOwnedPackagePathsFromGlobalBins(pm),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const candidatePath of candidatePackagePaths) {
|
||||||
|
if (!candidatePath) continue;
|
||||||
|
if (pathSetContains(currentPackagePaths, getComparablePaths(candidatePath))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function detectPackageManagerDetails() {
|
||||||
if (cachedDetectedPm) {
|
if (cachedDetectedPm) {
|
||||||
return cachedDetectedPm;
|
return {
|
||||||
|
packageManager: cachedDetectedPm,
|
||||||
|
reason: 'cached',
|
||||||
|
packagePath: getCurrentPackagePath(),
|
||||||
|
packageManagerCommand: resolvePackageManagerCommand(cachedDetectedPm),
|
||||||
|
globalNodeModulesRoot: getGlobalNodeModulesRoots(cachedDetectedPm)[0] || null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const forcedPm = process.env.OPENCHAMBER_PACKAGE_MANAGER?.trim();
|
const forcedPm = process.env.OPENCHAMBER_PACKAGE_MANAGER?.trim();
|
||||||
@@ -149,18 +342,44 @@ export function detectPackageManager() {
|
|||||||
const forcedPmCommand = resolvePackageManagerCommand(forcedPm);
|
const forcedPmCommand = resolvePackageManagerCommand(forcedPm);
|
||||||
if (isCommandAvailable(forcedPmCommand)) {
|
if (isCommandAvailable(forcedPmCommand)) {
|
||||||
cachedDetectedPm = forcedPm;
|
cachedDetectedPm = forcedPm;
|
||||||
return cachedDetectedPm;
|
return {
|
||||||
|
packageManager: cachedDetectedPm,
|
||||||
|
reason: 'forced-env',
|
||||||
|
packagePath: getCurrentPackagePath(),
|
||||||
|
packageManagerCommand: forcedPmCommand,
|
||||||
|
globalNodeModulesRoot: getGlobalNodeModulesRoots(cachedDetectedPm)[0] || null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strategy 1: Detect from runtime executable path (reliable for server-side updates)
|
// First prefer the package manager that demonstrably owns the current install.
|
||||||
const runtimePm = detectPackageManagerFromRuntimePath(process.execPath);
|
const installPathPm = detectPackageManagerFromCurrentInstallPath();
|
||||||
if (runtimePm && isCommandAvailable(resolvePackageManagerCommand(runtimePm))) {
|
if (installPathPm && packageManagerOwnsCurrentInstall(installPathPm)) {
|
||||||
cachedDetectedPm = runtimePm;
|
cachedDetectedPm = installPathPm;
|
||||||
return cachedDetectedPm;
|
return {
|
||||||
|
packageManager: cachedDetectedPm,
|
||||||
|
reason: 'install-path-owner',
|
||||||
|
packagePath: getCurrentPackagePath(),
|
||||||
|
packageManagerCommand: resolvePackageManagerCommand(cachedDetectedPm),
|
||||||
|
globalNodeModulesRoot: getGlobalNodeModulesRoots(cachedDetectedPm)[0] || null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strategy 2: Check user agent (most reliable during install)
|
const ownershipCandidates = ['pnpm', 'yarn', 'bun', 'npm'];
|
||||||
|
for (const candidate of ownershipCandidates) {
|
||||||
|
if (packageManagerOwnsCurrentInstall(candidate)) {
|
||||||
|
cachedDetectedPm = candidate;
|
||||||
|
return {
|
||||||
|
packageManager: cachedDetectedPm,
|
||||||
|
reason: 'global-root-owner',
|
||||||
|
packagePath: getCurrentPackagePath(),
|
||||||
|
packageManagerCommand: resolvePackageManagerCommand(cachedDetectedPm),
|
||||||
|
globalNodeModulesRoot: getGlobalNodeModulesRoots(cachedDetectedPm)[0] || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to weaker hints only when ownership cannot be established.
|
||||||
const userAgent = process.env.npm_config_user_agent || '';
|
const userAgent = process.env.npm_config_user_agent || '';
|
||||||
let hintedPm = null;
|
let hintedPm = null;
|
||||||
if (userAgent.startsWith('pnpm')) hintedPm = 'pnpm';
|
if (userAgent.startsWith('pnpm')) hintedPm = 'pnpm';
|
||||||
@@ -168,7 +387,7 @@ export function detectPackageManager() {
|
|||||||
else if (userAgent.startsWith('bun')) hintedPm = 'bun';
|
else if (userAgent.startsWith('bun')) hintedPm = 'bun';
|
||||||
else if (userAgent.startsWith('npm')) hintedPm = 'npm';
|
else if (userAgent.startsWith('npm')) hintedPm = 'npm';
|
||||||
|
|
||||||
// Strategy 3: Check execpath
|
// Check execpath.
|
||||||
const execPath = process.env.npm_execpath || '';
|
const execPath = process.env.npm_execpath || '';
|
||||||
if (!hintedPm) {
|
if (!hintedPm) {
|
||||||
if (execPath.includes('pnpm')) hintedPm = 'pnpm';
|
if (execPath.includes('pnpm')) hintedPm = 'pnpm';
|
||||||
@@ -177,39 +396,41 @@ export function detectPackageManager() {
|
|||||||
else if (execPath.includes('npm')) hintedPm = 'npm';
|
else if (execPath.includes('npm')) hintedPm = 'npm';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strategy 4: Detect from invoked binary path (works for bun global symlink installs)
|
// Detect from invoked binary path.
|
||||||
const invokedPm = detectPackageManagerFromInvocationPath(process.argv?.[1]);
|
const invokedPm = detectPackageManagerFromInvocationPath(process.argv?.[1]);
|
||||||
if (invokedPm && isCommandAvailable(resolvePackageManagerCommand(invokedPm))) {
|
|
||||||
cachedDetectedPm = invokedPm;
|
|
||||||
return cachedDetectedPm;
|
|
||||||
}
|
|
||||||
if (!hintedPm) {
|
if (!hintedPm) {
|
||||||
hintedPm = invokedPm;
|
hintedPm = invokedPm;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strategy 5: Analyze package location for PM-specific patterns
|
if (!hintedPm) {
|
||||||
try {
|
hintedPm = installPathPm;
|
||||||
const pkgPath = path.resolve(__dirname, '..', '..');
|
|
||||||
const pmFromPath = detectPackageManagerFromInstallPath(pkgPath);
|
|
||||||
if (pmFromPath && isCommandAvailable(resolvePackageManagerCommand(pmFromPath))) {
|
|
||||||
cachedDetectedPm = pmFromPath;
|
|
||||||
return cachedDetectedPm;
|
|
||||||
}
|
|
||||||
if (!hintedPm) {
|
|
||||||
hintedPm = pmFromPath;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Ignore path resolution errors
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the hinted PM actually owns the global install.
|
// Validate the hint against package visibility, but only after ownership checks failed.
|
||||||
// This avoids false positives (for example running via bunx while installed with npm).
|
|
||||||
if (hintedPm && isCommandAvailable(resolvePackageManagerCommand(hintedPm)) && isPackageInstalledWith(hintedPm)) {
|
if (hintedPm && isCommandAvailable(resolvePackageManagerCommand(hintedPm)) && isPackageInstalledWith(hintedPm)) {
|
||||||
cachedDetectedPm = hintedPm;
|
cachedDetectedPm = hintedPm;
|
||||||
return cachedDetectedPm;
|
return {
|
||||||
|
packageManager: cachedDetectedPm,
|
||||||
|
reason: 'hinted-visible-install',
|
||||||
|
packagePath: getCurrentPackagePath(),
|
||||||
|
packageManagerCommand: resolvePackageManagerCommand(cachedDetectedPm),
|
||||||
|
globalNodeModulesRoot: getGlobalNodeModulesRoots(cachedDetectedPm)[0] || null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strategy 6: Check which PM binaries are available and preferred
|
const runtimePm = detectPackageManagerFromRuntimePath(process.execPath);
|
||||||
|
if (runtimePm && isCommandAvailable(resolvePackageManagerCommand(runtimePm)) && isPackageInstalledWith(runtimePm)) {
|
||||||
|
cachedDetectedPm = runtimePm;
|
||||||
|
return {
|
||||||
|
packageManager: cachedDetectedPm,
|
||||||
|
reason: 'runtime-visible-install',
|
||||||
|
packagePath: getCurrentPackagePath(),
|
||||||
|
packageManagerCommand: resolvePackageManagerCommand(cachedDetectedPm),
|
||||||
|
globalNodeModulesRoot: getGlobalNodeModulesRoots(cachedDetectedPm)[0] || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last resort: pick a PM that can at least see the package.
|
||||||
const pmChecks = [
|
const pmChecks = [
|
||||||
{ name: 'pnpm', check: () => isCommandAvailable(resolvePackageManagerCommand('pnpm')) },
|
{ name: 'pnpm', check: () => isCommandAvailable(resolvePackageManagerCommand('pnpm')) },
|
||||||
{ name: 'yarn', check: () => isCommandAvailable(resolvePackageManagerCommand('yarn')) },
|
{ name: 'yarn', check: () => isCommandAvailable(resolvePackageManagerCommand('yarn')) },
|
||||||
@@ -222,13 +443,29 @@ export function detectPackageManager() {
|
|||||||
// Verify this PM actually has the package installed globally
|
// Verify this PM actually has the package installed globally
|
||||||
if (isPackageInstalledWith(name)) {
|
if (isPackageInstalledWith(name)) {
|
||||||
cachedDetectedPm = name;
|
cachedDetectedPm = name;
|
||||||
return cachedDetectedPm;
|
return {
|
||||||
|
packageManager: cachedDetectedPm,
|
||||||
|
reason: 'last-resort-visible-install',
|
||||||
|
packagePath: getCurrentPackagePath(),
|
||||||
|
packageManagerCommand: resolvePackageManagerCommand(cachedDetectedPm),
|
||||||
|
globalNodeModulesRoot: getGlobalNodeModulesRoots(cachedDetectedPm)[0] || null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cachedDetectedPm = 'npm';
|
cachedDetectedPm = 'npm';
|
||||||
return cachedDetectedPm;
|
return {
|
||||||
|
packageManager: cachedDetectedPm,
|
||||||
|
reason: 'default-fallback',
|
||||||
|
packagePath: getCurrentPackagePath(),
|
||||||
|
packageManagerCommand: resolvePackageManagerCommand(cachedDetectedPm),
|
||||||
|
globalNodeModulesRoot: getGlobalNodeModulesRoots(cachedDetectedPm)[0] || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function detectPackageManager() {
|
||||||
|
return detectPackageManagerDetails().packageManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectPackageManagerFromInstallPath(pkgPath) {
|
function detectPackageManagerFromInstallPath(pkgPath) {
|
||||||
|
|||||||
Reference in New Issue
Block a user