mirror of
http://10.0.2.1:3031/sauer/claude-code.git
synced 2026-06-30 12:46:58 +10:00
198 lines
30 KiB
TypeScript
198 lines
30 KiB
TypeScript
|
|
import * as React from 'react';
|
||
|
|
import { useEffect, useRef, useState } from 'react';
|
||
|
|
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js';
|
||
|
|
import { useInterval } from 'usehooks-ts';
|
||
|
|
import { useUpdateNotification } from '../hooks/useUpdateNotification.js';
|
||
|
|
import { Box, Text } from '../ink.js';
|
||
|
|
import { type AutoUpdaterResult, getLatestVersion, getMaxVersion, type InstallStatus, installGlobalPackage, shouldSkipVersion } from '../utils/autoUpdater.js';
|
||
|
|
import { getGlobalConfig, isAutoUpdaterDisabled } from '../utils/config.js';
|
||
|
|
import { logForDebugging } from '../utils/debug.js';
|
||
|
|
import { getCurrentInstallationType } from '../utils/doctorDiagnostic.js';
|
||
|
|
import { installOrUpdateClaudePackage, localInstallationExists } from '../utils/localInstaller.js';
|
||
|
|
import { removeInstalledSymlink } from '../utils/nativeInstaller/index.js';
|
||
|
|
import { gt, gte } from '../utils/semver.js';
|
||
|
|
import { getInitialSettings } from '../utils/settings/settings.js';
|
||
|
|
type Props = {
|
||
|
|
isUpdating: boolean;
|
||
|
|
onChangeIsUpdating: (isUpdating: boolean) => void;
|
||
|
|
onAutoUpdaterResult: (autoUpdaterResult: AutoUpdaterResult) => void;
|
||
|
|
autoUpdaterResult: AutoUpdaterResult | null;
|
||
|
|
showSuccessMessage: boolean;
|
||
|
|
verbose: boolean;
|
||
|
|
};
|
||
|
|
export function AutoUpdater({
|
||
|
|
isUpdating,
|
||
|
|
onChangeIsUpdating,
|
||
|
|
onAutoUpdaterResult,
|
||
|
|
autoUpdaterResult,
|
||
|
|
showSuccessMessage,
|
||
|
|
verbose
|
||
|
|
}: Props): React.ReactNode {
|
||
|
|
const [versions, setVersions] = useState<{
|
||
|
|
global?: string | null;
|
||
|
|
latest?: string | null;
|
||
|
|
}>({});
|
||
|
|
const [hasLocalInstall, setHasLocalInstall] = useState(false);
|
||
|
|
const updateSemver = useUpdateNotification(autoUpdaterResult?.version);
|
||
|
|
useEffect(() => {
|
||
|
|
void localInstallationExists().then(setHasLocalInstall);
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
// Track latest isUpdating value in a ref so the memoized checkForUpdates
|
||
|
|
// callback always sees the current value. Without this, the 30-minute
|
||
|
|
// interval fires with a stale closure where isUpdating is false, allowing
|
||
|
|
// a concurrent installGlobalPackage() to run while one is already in
|
||
|
|
// progress.
|
||
|
|
const isUpdatingRef = useRef(isUpdating);
|
||
|
|
isUpdatingRef.current = isUpdating;
|
||
|
|
const checkForUpdates = React.useCallback(async () => {
|
||
|
|
if (isUpdatingRef.current) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if ("production" === 'test' || "production" === 'development') {
|
||
|
|
logForDebugging('AutoUpdater: Skipping update check in test/dev environment');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const currentVersion = MACRO.VERSION;
|
||
|
|
const channel = getInitialSettings()?.autoUpdatesChannel ?? 'latest';
|
||
|
|
let latestVersion = await getLatestVersion(channel);
|
||
|
|
const isDisabled = isAutoUpdaterDisabled();
|
||
|
|
|
||
|
|
// Check if max version is set (server-side kill switch for auto-updates)
|
||
|
|
const maxVersion = await getMaxVersion();
|
||
|
|
if (maxVersion && latestVersion && gt(latestVersion, maxVersion)) {
|
||
|
|
logForDebugging(`AutoUpdater: maxVersion ${maxVersion} is set, capping update from ${latestVersion} to ${maxVersion}`);
|
||
|
|
if (gte(currentVersion, maxVersion)) {
|
||
|
|
logForDebugging(`AutoUpdater: current version ${currentVersion} is already at or above maxVersion ${maxVersion}, skipping update`);
|
||
|
|
setVersions({
|
||
|
|
global: currentVersion,
|
||
|
|
latest: latestVersion
|
||
|
|
});
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
latestVersion = maxVersion;
|
||
|
|
}
|
||
|
|
setVersions({
|
||
|
|
global: currentVersion,
|
||
|
|
latest: latestVersion
|
||
|
|
});
|
||
|
|
|
||
|
|
// Check if update needed and perform update
|
||
|
|
if (!isDisabled && currentVersion && latestVersion && !gte(currentVersion, latestVersion) && !shouldSkipVersion(latestVersion)) {
|
||
|
|
const startTime = Date.now();
|
||
|
|
onChangeIsUpdating(true);
|
||
|
|
|
||
|
|
// Remove native installer symlink since we're using JS-based updates
|
||
|
|
// But only if user hasn't migrated to native installation
|
||
|
|
const config = getGlobalConfig();
|
||
|
|
if (config.installMethod !== 'native') {
|
||
|
|
await removeInstalledSymlink();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Detect actual running installation type
|
||
|
|
const installationType = await getCurrentInstallationType();
|
||
|
|
logForDebugging(`AutoUpdater: Detected installation type: ${installationType}`);
|
||
|
|
|
||
|
|
// Skip update for development builds
|
||
|
|
if (installationType === 'development') {
|
||
|
|
logForDebugging('AutoUpdater: Cannot auto-update development build');
|
||
|
|
onChangeIsUpdating(false);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Choose the appropriate update method based on what's actually running
|
||
|
|
let installStatus: InstallStatus;
|
||
|
|
let updateMethod: 'local' | 'global';
|
||
|
|
if (installationType === 'npm-local') {
|
||
|
|
// Use local update for local installations
|
||
|
|
logForDebugging('AutoUpdater: Using local update method');
|
||
|
|
updateMethod = 'local';
|
||
|
|
installStatus = await installOrUpdateClaudePackage(channel);
|
||
|
|
} else if (installationType === 'npm-global') {
|
||
|
|
// Use global update for global installations
|
||
|
|
logForDebugging('AutoUpdater: Using global update method');
|
||
|
|
updateMethod = 'global';
|
||
|
|
installStatus = await installGlobalPackage();
|
||
|
|
} else if (installationType === 'native') {
|
||
|
|
// This shouldn't happen - native should use NativeAutoUpdater
|
||
|
|
logForDebugging('AutoUpdater: Unexpected native installation in non-native updater');
|
||
|
|
onChangeIsUpdating(false);
|
||
|
|
return;
|
||
|
|
} else {
|
||
|
|
// Fallback to config-based detection for unknown types
|
||
|
|
logForDebugging(`AutoUpdater: Unknown installation type, falling back to config`);
|
||
|
|
const isMigrated = config.installMethod === 'local';
|
||
|
|
updateMethod = isMigrated ? 'local' : 'global';
|
||
|
|
if (isMigrated) {
|
||
|
|
installStatus = await installOrUpdateClaudePackage(channel);
|
||
|
|
} else {
|
||
|
|
installStatus = await installGlobalPackage();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
onChangeIsUpdating(false);
|
||
|
|
if (installStatus === 'success') {
|
||
|
|
logEvent('tengu_auto_updater_success', {
|
||
|
|
fromVersion: currentVersion as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||
|
|
toVersion: latestVersion as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||
|
|
durationMs: Date.now() - startTime,
|
||
|
|
wasMigrated: updateMethod === 'local',
|
||
|
|
installationType: installationType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
logEvent('tengu_auto_updater_fail', {
|
||
|
|
fromVersion: currentVersion as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||
|
|
attemptedVersion: latestVersion as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||
|
|
status: installStatus as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||
|
|
durationMs: Date.now() - startTime,
|
||
|
|
wasMigrated: updateMethod === 'local',
|
||
|
|
installationType: installationType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||
|
|
});
|
||
|
|
}
|
||
|
|
onAutoUpdaterResult({
|
||
|
|
version: latestVersion,
|
||
|
|
status: installStatus
|
||
|
|
});
|
||
|
|
}
|
||
|
|
// isUpdating intentionally omitted from deps; we read isUpdatingRef
|
||
|
|
// instead so the guard is always current without changing callback
|
||
|
|
// identity (which would re-trigger the initial-check useEffect below).
|
||
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
|
|
// biome-ignore lint/correctness/useExhaustiveDependencies: isUpdating read via ref
|
||
|
|
}, [onAutoUpdaterResult]);
|
||
|
|
|
||
|
|
// Initial check
|
||
|
|
useEffect(() => {
|
||
|
|
void checkForUpdates();
|
||
|
|
}, [checkForUpdates]);
|
||
|
|
|
||
|
|
// Check every 30 minutes
|
||
|
|
useInterval(checkForUpdates, 30 * 60 * 1000);
|
||
|
|
if (!autoUpdaterResult?.version && (!versions.global || !versions.latest)) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
if (!autoUpdaterResult?.version && !isUpdating) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
return <Box flexDirection="row" gap={1}>
|
||
|
|
{verbose && <Text dimColor wrap="truncate">
|
||
|
|
globalVersion: {versions.global} · latestVersion:{' '}
|
||
|
|
{versions.latest}
|
||
|
|
</Text>}
|
||
|
|
{isUpdating ? <>
|
||
|
|
<Box>
|
||
|
|
<Text color="text" dimColor wrap="truncate">
|
||
|
|
Auto-updating…
|
||
|
|
</Text>
|
||
|
|
</Box>
|
||
|
|
</> : autoUpdaterResult?.status === 'success' && showSuccessMessage && updateSemver && <Text color="success" wrap="truncate">
|
||
|
|
✓ Update installed · Restart to apply
|
||
|
|
</Text>}
|
||
|
|
{(autoUpdaterResult?.status === 'install_failed' || autoUpdaterResult?.status === 'no_permissions') && <Text color="error" wrap="truncate">
|
||
|
|
✗ Auto-update failed · Try <Text bold>claude doctor</Text> or{' '}
|
||
|
|
<Text bold>
|
||
|
|
{hasLocalInstall ? `cd ~/.claude/local && npm update ${MACRO.PACKAGE_URL}` : `npm i -g ${MACRO.PACKAGE_URL}`}
|
||
|
|
</Text>
|
||
|
|
</Text>}
|
||
|
|
</Box>;
|
||
|
|
}
|
||
|
|
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsInVzZUVmZmVjdCIsInVzZVJlZiIsInVzZVN0YXRlIiwiQW5hbHl0aWNzTWV0YWRhdGFfSV9WRVJJRklFRF9USElTX0lTX05PVF9DT0RFX09SX0ZJTEVQQVRIUyIsImxvZ0V2ZW50IiwidXNlSW50ZXJ2YWwiLCJ1c2VVcGRhdGVOb3RpZmljYXRpb24iLCJCb3giLCJUZXh0IiwiQXV0b1VwZGF0ZXJSZXN1bHQiLCJnZXRMYXRlc3RWZXJzaW9uIiwiZ2V0TWF4VmVyc2lvbiIsIkluc3RhbGxTdGF0dXMiLCJpbnN0YWxsR2xvYmFsUGFja2FnZSIsInNob3VsZFNraXBWZXJzaW9uIiwiZ2V0R2xvYmFsQ29uZmlnIiwiaXNBdXRvVXBkYXRlckRpc2FibGVkIiwibG9nRm9yRGVidWdnaW5nIiwiZ2V0Q3VycmVudEluc3RhbGxhdGlvblR5cGUiLCJpbnN0YWxsT3JVcGRhdGVDbGF1ZGVQYWNrYWdlIiwibG9jYWxJbnN0YWxsYXRpb25FeGlzdHMiLCJyZW1vdmVJbnN0YWxsZWRTeW1saW5rIiwiZ3QiLCJndGUiLCJnZXRJbml0aWFsU2V0dGluZ3MiLCJQcm9wcyIsImlzVXBkYXRpbmciLCJvbkNoYW5nZUlzVXBkYXRpbmciLCJvbkF1dG9VcGRhdGVyUmVzdWx0IiwiYXV0b1VwZGF0ZXJSZXN1bHQiLCJzaG93U3VjY2Vzc01lc3NhZ2UiLCJ2ZXJib3NlIiwiQXV0b1VwZGF0ZXIiLCJSZWFjdE5vZGUiLCJ2ZXJzaW9ucyIsInNldFZlcnNpb25zIiwiZ2xvYmFsIiwibGF0ZXN0IiwiaGFzTG9jYWxJbnN0YWxsIiwic2V0SGFzTG9jYWxJbnN0YWxsIiwidXBkYXRlU2VtdmVyIiwidmVyc2lvbiIsInRoZW4iLCJpc1VwZGF0aW5nUmVmIiwiY3VycmVudCIsImNoZWNrRm9yVXBkYXRlcyIsInVzZUNhbGxiYWNrIiwiY3VycmVudFZlcnNpb24iLCJNQUNSTyIsIlZFUlNJT04iLCJjaGFubmVsIiwiYXV0b1VwZGF0ZXNDaGFubmVsIiwibGF0ZXN0VmVyc2lvbiIsImlzRGlzYWJsZWQiLCJtYXhWZXJzaW9uIiwic3RhcnRUaW1lIiwiRGF0ZSIsIm5vdyIsImNvbmZpZyIsImluc3RhbGxNZXRob2QiLCJpbnN0YWxsYXRpb25UeXBlIiwiaW5zdGFsbFN0YXR1cyIsInVwZGF0ZU1ldGhvZCIsImlzTWlncmF0ZWQiLCJmcm9tVmVyc2lvbiIsInRvVmVyc2lvbiIsImR1cmF0aW9uTXMiLCJ3YXNNaWdyYXRlZCIsImF0dGVtcHRlZFZlcnNpb24iLCJzdGF0dXMiLCJQQUNLQUdFX1VSTCJdLCJzb3VyY2VzIjpbIkF1dG9VcGRhdGVyLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgKiBhcyBSZWFjdCBmcm9tICdyZWFjdCdcbmltcG9ydCB7IHVzZUVmZmVjdCwgdXNlUmVmLCB1c2VTdGF0ZSB9IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHtcbiAgdHlwZSBBbmFseXRpY3NNZXRhZGF0YV9JX1ZFUklGSUVEX1RISVNfSVNfTk9UX0NPREVfT1JfRklMRVBBVEhTLFxuICBsb2dFdmVudCxcbn0gZnJvbSAnc3JjL3NlcnZpY2VzL2FuYWx5dGljcy9pbmRleC5qcydcbmltcG9ydCB7IHVzZUludGVydmFsIH0gZnJvbSAndXNlaG9va3MtdHMnXG5pbXBvcnQgeyB1c2VVcGRhdGVOb3RpZmljYXRpb24gfSBmcm9tICcuLi9ob29rcy91c2VVcGRhdGVOb3RpZmljYXRpb24uanMnXG5pbXBvcnQgeyBCb3gsIFRleHQgfSBmcm9tICcuLi9pbmsuanMnXG5pbXBvcnQge1xuICB0eXBlIEF1dG9VcGRhdGVyUmVzdWx0LFxuICBnZXRMYXRlc3RWZXJzaW9uLFxuICBnZXRNYXhWZXJzaW9uLFxuICB0eXBlIEluc3RhbGxTdGF0dXMsXG4gIGluc3RhbGxHbG9iYWxQYWNrYWdlLFxuICBzaG91bGRTa2lwVmVyc2lvbixcbn0gZnJvbSAnLi4vdXRpbHMvYXV0b1VwZGF0ZXIuanMnXG5pbXBvcnQgeyBnZXRHbG9iYWxDb25maWcsIGlzQXV0b1VwZGF0ZXJEaXNhYmxlZCB9IGZyb20gJy4uL3V0aWxzL2NvbmZpZy5qcydcbmltcG9ydCB7IGxvZ0ZvckRlYnVnZ2luZyB9IGZyb20gJy4uL3V0aWxzL2RlYnVnLmpzJ1xuaW1wb3J0IHsgZ2V0Q3VycmVudEluc3RhbGxhdGlvblR5cGUgfSBmcm9tICcuLi91dGlscy9kb2N0b3JEaWFnbm9zdGljLmpzJ1xuaW1wb3J0IHtcbiAgaW5zdGFsbE9yVXBkYXRlQ2xhdWRlUGFja2FnZSxcbiAgbG9jYWxJbnN0YWxsYXRpb25FeGlzdHMsXG59IGZyb20gJy4uL3V0aWxzL2xvY2FsSW5zdGFsbGVyLmpzJ1xuaW1wb3J0IHsgcmVtb3ZlSW5zdGFsbGVkU3ltbGluayB9IGZyb20gJy4uL3V0aWxzL25hdGl2ZUluc3RhbGxlci9pbmRleC5qcydcbmltcG9ydCB7IGd0LCBndGUgfSBmcm9tICcuLi91dGlscy9zZW12ZXIuanMnXG5pbXBvcnQgeyBnZXRJbml0aWFsU2V0dGluZ3MgfSBmcm9tICcuLi91dGlscy9zZXR0aW5ncy9zZXR0aW5ncy5qcydcblxudHlwZSBQcm9wcyA9IHtcbiAgaXNVcGRhdGluZzogYm9vbGVhblxuICBvbkNoYW5nZUlzVXBkYXRpbmc6IChpc1VwZGF0aW5nOiBib29sZWFuKSA9PiB2b2lkXG4gIG9uQXV0b1VwZGF0ZXJSZXN1bHQ6IChhdXRvVXBkYXRlclJlc3VsdDogQXV0b1VwZGF0ZXJSZXN1bHQpID0+IHZvaWRcbiAgYXV0b1VwZGF0ZXJSZXN1bHQ6IEF1dG9VcGRhdGVyUmVzdWx0IHwgbnVsbFxuICBzaG93U3VjY2Vzc01lc3NhZ2U6IGJvb2xlYW5cbiAgdmVyYm9zZTogYm9vbGVhblxufVxuXG5leHBvcnQgZnVuY3Rpb24gQXV0b1VwZGF0ZXIoe1xuICBpc1VwZGF0aW5nLFxuICBvbkNoYW5nZUlzVXBkYXRpbmcsXG4gIG9uQXV0b1VwZGF0ZXJSZXN1bHQsXG4gIGF1dG9VcGRhdGVyUmVzdWx0LFxuICBzaG93U3VjY2Vzc01lc3NhZ2UsXG4gIHZlcmJvc2UsXG59OiBQcm9wcyk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIGNvbnN0IFt2ZXJzaW9ucywgc2V0VmVyc2lvbnNdID0gdXNlU3RhdGU8e1xuICAgIGdsb2JhbD86IHN0cmluZyB8IG51bGxcbiAgICBsYXRlc3Q/OiBzdHJpbmcgfCBudWxsXG4gIH0+KHt9KVxuICBjb25zdCBbaGFzTG9jYWxJbnN0YWxsLCBzZXRIYXNMb2NhbEluc3RhbGxdID0gdXNlU3RhdGUoZmFsc2UpXG4gIGNvbnN0IHVwZGF0ZVNlbXZlciA9IHVzZVVwZGF0ZU5vdGlmaWNhdGlvbihhdXRvVXBkYXRlclJlc3VsdD8udmVyc2lvbilcblxuICB1c2VFZmZlY3QoKCkgPT4ge1xuICAgIHZvaWQgbG9jYWxJbnN0YWxsYXRpb25FeGlzdHMoKS50aGVuKHNldEhhc0xvY2FsSW5zdGFsbClcbiA
|