fix: harden self-update flow and improve chat message readability (#562)
* fix: resolve Windows CLI module loading on absolute paths * fix: improve chat tool rows layout and timestamp readability * fix: make web update restart more reliable * fix: make web self-update detect package manager correctly
This commit is contained in:
committed by
GitHub
parent
7b5fd9e70c
commit
8bfbed0e68
@@ -19,7 +19,21 @@ const CHANGELOG_URL = 'https://raw.githubusercontent.com/btriapitsyn/openchamber
|
||||
* 4. Fall back to npm
|
||||
*/
|
||||
export function detectPackageManager() {
|
||||
// Strategy 1: Check user agent (most reliable during install)
|
||||
const forcedPm = process.env.OPENCHAMBER_PACKAGE_MANAGER?.trim();
|
||||
if (forcedPm && ['npm', 'pnpm', 'yarn', 'bun'].includes(forcedPm)) {
|
||||
const forcedPmCommand = resolvePackageManagerCommand(forcedPm);
|
||||
if (isCommandAvailable(forcedPmCommand)) {
|
||||
return forcedPm;
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 1: Detect from runtime executable path (reliable for server-side updates)
|
||||
const runtimePm = detectPackageManagerFromRuntimePath(process.execPath);
|
||||
if (runtimePm && isCommandAvailable(resolvePackageManagerCommand(runtimePm))) {
|
||||
return runtimePm;
|
||||
}
|
||||
|
||||
// Strategy 2: Check user agent (most reliable during install)
|
||||
const userAgent = process.env.npm_config_user_agent || '';
|
||||
let hintedPm = null;
|
||||
if (userAgent.startsWith('pnpm')) hintedPm = 'pnpm';
|
||||
@@ -27,7 +41,7 @@ export function detectPackageManager() {
|
||||
else if (userAgent.startsWith('bun')) hintedPm = 'bun';
|
||||
else if (userAgent.startsWith('npm')) hintedPm = 'npm';
|
||||
|
||||
// Strategy 2: Check execpath
|
||||
// Strategy 3: Check execpath
|
||||
const execPath = process.env.npm_execpath || '';
|
||||
if (!hintedPm) {
|
||||
if (execPath.includes('pnpm')) hintedPm = 'pnpm';
|
||||
@@ -36,34 +50,41 @@ export function detectPackageManager() {
|
||||
else if (execPath.includes('npm')) hintedPm = 'npm';
|
||||
}
|
||||
|
||||
// Strategy 3: Analyze package location for PM-specific patterns
|
||||
// Strategy 4: Detect from invoked binary path (works for bun global symlink installs)
|
||||
const invokedPm = detectPackageManagerFromInvocationPath(process.argv?.[1]);
|
||||
if (invokedPm && isCommandAvailable(resolvePackageManagerCommand(invokedPm))) {
|
||||
return invokedPm;
|
||||
}
|
||||
if (!hintedPm) {
|
||||
try {
|
||||
const pkgPath = path.resolve(__dirname, '..', '..');
|
||||
if (pkgPath.includes('.pnpm')) hintedPm = 'pnpm';
|
||||
else if (pkgPath.includes('/.yarn/') || pkgPath.includes('\\.yarn\\')) hintedPm = 'yarn';
|
||||
else if (pkgPath.includes('/.bun/') || pkgPath.includes('\\.bun\\')) hintedPm = 'bun';
|
||||
} catch {
|
||||
// Ignore path resolution errors
|
||||
hintedPm = invokedPm;
|
||||
}
|
||||
|
||||
// Strategy 5: Analyze package location for PM-specific patterns
|
||||
try {
|
||||
const pkgPath = path.resolve(__dirname, '..', '..');
|
||||
const pmFromPath = detectPackageManagerFromInstallPath(pkgPath);
|
||||
if (pmFromPath && isCommandAvailable(resolvePackageManagerCommand(pmFromPath))) {
|
||||
return pmFromPath;
|
||||
}
|
||||
if (!hintedPm) {
|
||||
hintedPm = pmFromPath;
|
||||
}
|
||||
} catch {
|
||||
// Ignore path resolution errors
|
||||
}
|
||||
|
||||
// Validate the hinted PM actually owns the global install.
|
||||
// This avoids false positives (for example running via bunx while installed with npm).
|
||||
if (hintedPm && isCommandAvailable(hintedPm) && isPackageInstalledWith(hintedPm)) {
|
||||
if (hintedPm && isCommandAvailable(resolvePackageManagerCommand(hintedPm)) && isPackageInstalledWith(hintedPm)) {
|
||||
return hintedPm;
|
||||
}
|
||||
|
||||
if (isCommandAvailable('npm') && isPackageInstalledWith('npm')) {
|
||||
return 'npm';
|
||||
}
|
||||
|
||||
// Strategy 4: Check which PM binaries are available and preferred
|
||||
// Strategy 6: Check which PM binaries are available and preferred
|
||||
const pmChecks = [
|
||||
{ name: 'npm', check: () => isCommandAvailable('npm') },
|
||||
{ name: 'pnpm', check: () => isCommandAvailable('pnpm') },
|
||||
{ name: 'yarn', check: () => isCommandAvailable('yarn') },
|
||||
{ name: 'bun', check: () => isCommandAvailable('bun') },
|
||||
{ name: 'pnpm', check: () => isCommandAvailable(resolvePackageManagerCommand('pnpm')) },
|
||||
{ name: 'yarn', check: () => isCommandAvailable(resolvePackageManagerCommand('yarn')) },
|
||||
{ name: 'bun', check: () => isCommandAvailable(resolvePackageManagerCommand('bun')) },
|
||||
{ name: 'npm', check: () => isCommandAvailable(resolvePackageManagerCommand('npm')) },
|
||||
];
|
||||
|
||||
for (const { name, check } of pmChecks) {
|
||||
@@ -78,6 +99,74 @@ export function detectPackageManager() {
|
||||
return 'npm';
|
||||
}
|
||||
|
||||
function detectPackageManagerFromInstallPath(pkgPath) {
|
||||
if (!pkgPath) return null;
|
||||
const normalized = pkgPath.replace(/\\/g, '/').toLowerCase();
|
||||
if (normalized.includes('/.pnpm/') || normalized.includes('/pnpm/')) return 'pnpm';
|
||||
if (normalized.includes('/.yarn/')) return 'yarn';
|
||||
if (normalized.includes('/.bun/') || normalized.includes('/bun/install/')) return 'bun';
|
||||
if (normalized.includes('/node_modules/')) return 'npm';
|
||||
return null;
|
||||
}
|
||||
|
||||
function detectPackageManagerFromRuntimePath(runtimePath) {
|
||||
if (!runtimePath || typeof runtimePath !== 'string') return null;
|
||||
const normalized = runtimePath.replace(/\\/g, '/').toLowerCase();
|
||||
if (normalized.includes('/.bun/bin/bun') || normalized.endsWith('/bun') || normalized.endsWith('/bun.exe')) {
|
||||
return 'bun';
|
||||
}
|
||||
if (normalized.includes('/pnpm/')) return 'pnpm';
|
||||
if (normalized.includes('/yarn/')) return 'yarn';
|
||||
if (normalized.includes('/node') || normalized.endsWith('/node.exe')) return 'npm';
|
||||
return null;
|
||||
}
|
||||
|
||||
function detectPackageManagerFromInvocationPath(invokedPath) {
|
||||
if (!invokedPath || typeof invokedPath !== 'string') return null;
|
||||
const normalized = invokedPath.replace(/\\/g, '/').toLowerCase();
|
||||
if (normalized.includes('/.bun/bin/')) return 'bun';
|
||||
if (normalized.includes('/.pnpm/')) return 'pnpm';
|
||||
if (normalized.includes('/.yarn/')) return 'yarn';
|
||||
return null;
|
||||
}
|
||||
|
||||
function getPackageManagerCommandCandidates(pm) {
|
||||
const candidates = [];
|
||||
if (pm === 'bun') {
|
||||
const bunExecutable = process.platform === 'win32' ? 'bun.exe' : 'bun';
|
||||
if (process.env.BUN_INSTALL) {
|
||||
candidates.push(path.join(process.env.BUN_INSTALL, 'bin', bunExecutable));
|
||||
}
|
||||
if (process.env.HOME) {
|
||||
candidates.push(path.join(process.env.HOME, '.bun', 'bin', bunExecutable));
|
||||
}
|
||||
if (process.env.USERPROFILE) {
|
||||
candidates.push(path.join(process.env.USERPROFILE, '.bun', 'bin', bunExecutable));
|
||||
}
|
||||
}
|
||||
candidates.push(pm);
|
||||
return [...new Set(candidates.filter(Boolean))];
|
||||
}
|
||||
|
||||
function resolvePackageManagerCommand(pm) {
|
||||
const candidates = getPackageManagerCommandCandidates(pm);
|
||||
for (const candidate of candidates) {
|
||||
if (isCommandAvailable(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return pm;
|
||||
}
|
||||
|
||||
function quoteCommand(command) {
|
||||
if (!command) return command;
|
||||
if (!/\s/.test(command)) return command;
|
||||
if (process.platform === 'win32') {
|
||||
return `"${command.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return `'${command.replace(/'/g, "'\\''")}'`;
|
||||
}
|
||||
|
||||
function isCommandAvailable(command) {
|
||||
try {
|
||||
const result = spawnSync(command, ['--version'], {
|
||||
@@ -93,6 +182,7 @@ function isCommandAvailable(command) {
|
||||
|
||||
function isPackageInstalledWith(pm) {
|
||||
try {
|
||||
const pmCommand = resolvePackageManagerCommand(pm);
|
||||
let args;
|
||||
switch (pm) {
|
||||
case 'pnpm':
|
||||
@@ -108,7 +198,7 @@ function isPackageInstalledWith(pm) {
|
||||
args = ['list', '-g', '--depth=0', PACKAGE_NAME];
|
||||
}
|
||||
|
||||
const result = spawnSync(pm, args, {
|
||||
const result = spawnSync(pmCommand, args, {
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: 10000,
|
||||
@@ -125,15 +215,16 @@ function isPackageInstalledWith(pm) {
|
||||
* Get the update command for the detected package manager
|
||||
*/
|
||||
export function getUpdateCommand(pm = detectPackageManager()) {
|
||||
const pmCommand = quoteCommand(resolvePackageManagerCommand(pm));
|
||||
switch (pm) {
|
||||
case 'pnpm':
|
||||
return `pnpm add -g ${PACKAGE_NAME}@latest`;
|
||||
return `${pmCommand} add -g ${PACKAGE_NAME}@latest`;
|
||||
case 'yarn':
|
||||
return `yarn global add ${PACKAGE_NAME}@latest`;
|
||||
return `${pmCommand} global add ${PACKAGE_NAME}@latest`;
|
||||
case 'bun':
|
||||
return `bun add -g ${PACKAGE_NAME}@latest`;
|
||||
return `${pmCommand} add -g ${PACKAGE_NAME}@latest`;
|
||||
default:
|
||||
return `npm install -g ${PACKAGE_NAME}@latest`;
|
||||
return `${pmCommand} install -g ${PACKAGE_NAME}@latest`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,8 +350,7 @@ export function executeUpdate(pm = detectPackageManager()) {
|
||||
console.log(`Updating ${PACKAGE_NAME} using ${pm}...`);
|
||||
console.log(`Running: ${command}`);
|
||||
|
||||
const [cmd, ...args] = command.split(' ');
|
||||
const result = spawnSync(cmd, args, {
|
||||
const result = spawnSync(command, {
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user