Merge pull request #140 from Askhz/feat/new-auto-update

feat: add new standalone auto-update mechanism
This commit is contained in:
Askhz 2026-05-21 14:18:28 +08:00 committed by GitHub
commit c3837c308d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 1780 additions and 1566 deletions

3132
bun.lock

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,63 @@
import { checkNewAutoUpdate } from 'src/utils/newAutoUpdater.js'
import { useNotifications } from 'src/context/notifications.js'
import { useStartupNotification } from './useStartupNotification.js'
export function useNewAutoUpdateCheck(): void {
const { addNotification } = useNotifications()
useStartupNotification(async () => {
const result = await checkNewAutoUpdate({
onBeforeInstall: version => {
addNotification({
key: 'new-auto-update-installing',
text: `Latest version v${version} released, auto-upgrading...`,
priority: 'low' as const,
timeoutMs: 30_000,
})
},
})
if (result.action === 'skip') return null
if (result.action === 'notify' && result.latestVersion) {
const typeLabel =
result.releaseType === 'major'
? 'Major'
: result.releaseType === 'minor'
? 'Minor'
: 'Patch'
return {
key: 'new-auto-update-notification',
text: `${typeLabel} update available: ${result.currentVersion}${result.latestVersion}. Run \`csc update\` to upgrade.`,
priority: 'low' as const,
timeoutMs: 30_000,
color: 'warning',
invalidates: ['new-auto-update-installing'],
}
}
if (result.action === 'installed' && result.latestVersion) {
return {
key: 'new-auto-update-installed',
text: `Updated to v${result.latestVersion}. Please restart to apply.`,
priority: 'low' as const,
timeoutMs: 30_000,
color: 'warning',
invalidates: ['new-auto-update-installing'],
}
}
if (result.action === 'failed' && result.latestVersion) {
return {
key: 'new-auto-update-failed',
text: `Auto-update to v${result.latestVersion} failed. Run \`csc update\` to upgrade manually.`,
priority: 'medium' as const,
timeoutMs: 30_000,
color: 'warning',
invalidates: ['new-auto-update-installing'],
}
}
return null
})
}

View File

@ -425,6 +425,7 @@ import { FeedbackSurvey } from 'src/components/FeedbackSurvey/FeedbackSurvey.js'
import { useInstallMessages } from 'src/hooks/notifs/useInstallMessages.js';
import { useAwaySummary } from 'src/hooks/useAwaySummary.js';
import { useChromeExtensionNotification } from 'src/hooks/useChromeExtensionNotification.js';
import { useNewAutoUpdateCheck } from 'src/hooks/notifs/useNewAutoUpdateCheck.js';
import { useOfficialMarketplaceNotification } from 'src/hooks/useOfficialMarketplaceNotification.js';
import { usePromptsFromClaudeInChrome } from 'src/hooks/usePromptsFromClaudeInChrome.js';
import { getTipToShowOnSpinner, recordShownTip } from 'src/services/tips/tipScheduler.js';
@ -1034,6 +1035,7 @@ export function REPL({
useInstallMessages();
useChromeExtensionNotification();
useOfficialMarketplaceNotification();
useNewAutoUpdateCheck();
useLspInitializationNotification();
useTeammateLifecycleNotification();
const { recommendation: lspRecommendation, handleResponse: handleLspResponse } = useLspPluginRecommendation();

149
src/utils/newAutoUpdater.ts Normal file
View File

@ -0,0 +1,149 @@
/**
* (newAutoUpdater)
*
* CS (opencode) upgrade.ts
* getLatestVersion / installGlobalPackage / semver
*
* :
* - NODE_ENV=development
* - DISABLE_CSC_AUTOUPDATER=1
* -
* - patch (npm install -g)
* - minor/major
* - CSC_AUTOUPDATER_NOTIFY_ONLY=1
*/
import { type ReleaseChannel, getGlobalConfig } from './config.js'
import { getLatestVersion, installGlobalPackage } from './autoUpdater.js'
import { getInitialSettings } from './settings/settings.js'
import { gt } from './semver.js'
import { logForDebugging } from './debug.js'
export type NewAutoUpdateAction = 'skip' | 'notify' | 'installed' | 'failed'
export type NewAutoUpdateResult = {
action: NewAutoUpdateAction
currentVersion: string
latestVersion: string | null
releaseType?: 'patch' | 'minor' | 'major'
errorMessage?: string
}
export function isNewAutoUpdaterDisabled(): boolean {
if (process.env.NODE_ENV === 'development') return true
if (
process.env.DISABLE_CSC_AUTOUPDATER === '1' ||
process.env.DISABLE_CSC_AUTOUPDATER === 'true'
) {
return true
}
return false
}
export function isNewAutoUpdateNotifyOnly(): boolean {
return (
process.env.CSC_AUTOUPDATER_NOTIFY_ONLY === '1' ||
process.env.CSC_AUTOUPDATER_NOTIFY_ONLY === 'true'
)
}
function parseSemverParts(version: string): {
major: number
minor: number
} {
const parts = version.split('.')
return {
major: Number.parseInt(parts[0] ?? '0', 10),
minor: Number.parseInt(parts[1] ?? '0', 10),
}
}
function getReleaseType(
current: string,
latest: string,
): 'patch' | 'minor' | 'major' {
const curr = parseSemverParts(current)
const next = parseSemverParts(latest)
if (next.major > curr.major) return 'major'
if (next.minor > curr.minor) return 'minor'
return 'patch'
}
export type NewAutoUpdateCallbacks = {
onBeforeInstall?: (version: string) => void
}
export async function checkNewAutoUpdate(
callbacks?: NewAutoUpdateCallbacks,
): Promise<NewAutoUpdateResult> {
const currentVersion = MACRO.VERSION
logForDebugging(`[newAutoUpdater] checking, current: ${currentVersion}`)
if (isNewAutoUpdaterDisabled()) {
return { action: 'skip', currentVersion, latestVersion: null }
}
// 获取 autoUpdatesChannel 配置,默认 'latest'
let channel: ReleaseChannel = 'latest'
try {
const settings = getInitialSettings()
if (settings.autoUpdatesChannel === 'stable') {
channel = 'stable'
}
} catch {
// settings 读取失败使用默认值
}
const latestVersion = await getLatestVersion(channel)
logForDebugging(`[newAutoUpdater] latest: ${JSON.stringify(latestVersion)}`)
if (!latestVersion) {
return { action: 'skip', currentVersion, latestVersion: null }
}
if (!gt(latestVersion, currentVersion)) {
logForDebugging(`[newAutoUpdater] up to date (${currentVersion} >= ${latestVersion})`)
return { action: 'skip', currentVersion, latestVersion }
}
const releaseType = getReleaseType(currentVersion, latestVersion)
const notifyOnly = isNewAutoUpdateNotifyOnly()
logForDebugging(`[newAutoUpdater] type: ${releaseType}, notifyOnly: ${notifyOnly}`)
// minor/major 或 notifyOnly 模式 → 仅通知
if (releaseType !== 'patch' || notifyOnly) {
return { action: 'notify', currentVersion, latestVersion, releaseType }
}
// patch → 尝试自动安装 (仅 npm-global)
const config = getGlobalConfig()
if (config.installMethod !== 'global') {
logForDebugging(`[newAutoUpdater] skip install, method: ${config.installMethod}`)
return { action: 'notify', currentVersion, latestVersion, releaseType }
}
callbacks?.onBeforeInstall?.(latestVersion)
const result = await installGlobalPackage(latestVersion)
if (result.status === 'success') {
logForDebugging('[newAutoUpdater] installed successfully')
return {
action: 'installed',
currentVersion,
latestVersion,
releaseType,
}
}
logForDebugging(
`[newAutoUpdater] install FAILED — category: ${result.errorCategory ?? 'unknown'}, suggestion: ${result.suggestion ?? 'none'}, stderr: ${result.npmStderr ?? 'none'}`,
{ level: 'error' },
)
return {
action: 'failed',
currentVersion,
latestVersion,
releaseType,
errorMessage: result.suggestion ?? result.errorCategory ?? 'Install failed',
}
}