Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
12 kB · 360 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361import axios from 'axios'import { mkdir, readFile, writeFile } from 'fs/promises'import { dirname, join } from 'path'import { coerce } from 'semver'import { getIsNonInteractiveSession } from '../bootstrap/state.js'import { getGlobalConfig, saveGlobalConfig } from './config.js'import { getClaudeConfigHomeDir } from './envUtils.js'import { toError } from './errors.js'import { logError } from './log.js'import { isEssentialTrafficOnly } from './privacyLevel.js'import { gt } from './semver.js'
const MAX_RELEASE_NOTES_SHOWN = 5
/** * We fetch the changelog from GitHub instead of bundling it with the build. * * This is necessary because Ink's static rendering makes it difficult to * dynamically update/show components after initial render. By storing the * changelog in config, we ensure it's available on the next startup without * requiring a full re-render of the current UI. * * The flow is: * 1. User updates to a new version * 2. We fetch the changelog in the background and store it in config * 3. Next time the user starts Claude, the cached changelog is available immediately */export const CHANGELOG_URL = 'https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md'const RAW_CHANGELOG_URL = 'https://raw.githubusercontent.com/anthropics/claude-code/refs/heads/main/CHANGELOG.md'
/** * Get the path for the cached changelog file. * The changelog is stored at ~/.claude/cache/changelog.md */function getChangelogCachePath(): string { return join(getClaudeConfigHomeDir(), 'cache', 'changelog.md')}
// In-memory cache populated by async reads. Sync callers (React render, sync// helpers) read from this cache after setup.ts awaits checkForReleaseNotes().let changelogMemoryCache: string | null = null
/** @internal exported for tests */export function _resetChangelogCacheForTesting(): void { changelogMemoryCache = null}
/** * Migrate changelog from old config-based storage to file-based storage. * This should be called once at startup to ensure the migration happens * before any other config saves that might re-add the deprecated field. */export async function migrateChangelogFromConfig(): Promise<void> { const config = getGlobalConfig() if (!config.cachedChangelog) { return }
const cachePath = getChangelogCachePath()
// If cache file doesn't exist, create it from old config try { await mkdir(dirname(cachePath), { recursive: true }) await writeFile(cachePath, config.cachedChangelog, { encoding: 'utf-8', flag: 'wx', // Write only if file doesn't exist }) } catch { // File already exists, which is fine - skip silently }
// Remove the deprecated field from config saveGlobalConfig(({ cachedChangelog: _, ...rest }) => rest)}
/** * Fetch the changelog from GitHub and store it in cache file * This runs in the background and doesn't block the UI */export async function fetchAndStoreChangelog(): Promise<void> { // Skip in noninteractive mode if (getIsNonInteractiveSession()) { return }
// Skip network requests if nonessential traffic is disabled if (isEssentialTrafficOnly()) { return }
const response = await axios.get(RAW_CHANGELOG_URL) if (response.status === 200) { const changelogContent = response.data
// Skip write if content unchanged — writing Date.now() defeats the // dirty-check in saveGlobalConfig since the timestamp always differs. if (changelogContent === changelogMemoryCache) { return }
const cachePath = getChangelogCachePath()
// Ensure cache directory exists await mkdir(dirname(cachePath), { recursive: true })
// Write changelog to cache file await writeFile(cachePath, changelogContent, { encoding: 'utf-8' }) changelogMemoryCache = changelogContent
// Update timestamp in config const changelogLastFetched = Date.now() saveGlobalConfig(current => ({ ...current, changelogLastFetched, })) }}
/** * Get the stored changelog from cache file if available. * Populates the in-memory cache for subsequent sync reads. * @returns The cached changelog content or empty string if not available */export async function getStoredChangelog(): Promise<string> { if (changelogMemoryCache !== null) { return changelogMemoryCache } const cachePath = getChangelogCachePath() try { const content = await readFile(cachePath, 'utf-8') changelogMemoryCache = content return content } catch { changelogMemoryCache = '' return '' }}
/** * Synchronous accessor for the changelog, reading only from the in-memory cache. * Returns empty string if the async getStoredChangelog() hasn't been called yet. * Intended for React render paths where async is not possible; setup.ts ensures * the cache is populated before first render via `await checkForReleaseNotes()`. */export function getStoredChangelogFromMemory(): string { return changelogMemoryCache ?? ''}
/** * Parses a changelog string in markdown format into a structured format * @param content - The changelog content string * @returns Record mapping version numbers to arrays of release notes */export function parseChangelog(content: string): Record<string, string[]> { try { if (!content) return {}
// Parse the content const releaseNotes: Record<string, string[]> = {}
// Split by heading lines (## X.X.X) const sections = content.split(/^## /gm).slice(1) // Skip the first section which is the header
for (const section of sections) { const lines = section.trim().split('\n') if (lines.length === 0) continue
// Extract version from the first line // Handle both "1.2.3" and "1.2.3 - YYYY-MM-DD" formats const versionLine = lines[0] if (!versionLine) continue
// First part before any dash is the version const version = versionLine.split(' - ')[0]?.trim() || '' if (!version) continue
// Extract bullet points const notes = lines .slice(1) .filter(line => line.trim().startsWith('- ')) .map(line => line.trim().substring(2).trim()) .filter(Boolean)
if (notes.length > 0) { releaseNotes[version] = notes } }
return releaseNotes } catch (error) { logError(toError(error)) return {} }}
/** * Gets release notes to show based on the previously seen version. * Shows up to MAX_RELEASE_NOTES_SHOWN items total, prioritizing the most recent versions. * * @param currentVersion - The current app version * @param previousVersion - The last version where release notes were seen (or null if first time) * @param readChangelog - Function to read the changelog (defaults to readChangelogFile) * @returns Array of release notes to display */export function getRecentReleaseNotes( currentVersion: string, previousVersion: string | null | undefined, changelogContent: string = getStoredChangelogFromMemory(),): string[] { try { const releaseNotes = parseChangelog(changelogContent)
// Strip SHA from both versions to compare only the base versions const baseCurrentVersion = coerce(currentVersion) const basePreviousVersion = previousVersion ? coerce(previousVersion) : null
if ( !basePreviousVersion || (baseCurrentVersion && gt(baseCurrentVersion.version, basePreviousVersion.version)) ) { // Get all versions that are newer than the last seen version return Object.entries(releaseNotes) .filter( ([version]) => !basePreviousVersion || gt(version, basePreviousVersion.version), ) .sort(([versionA], [versionB]) => (gt(versionA, versionB) ? -1 : 1)) // Sort newest first .flatMap(([_, notes]) => notes) .filter(Boolean) .slice(0, MAX_RELEASE_NOTES_SHOWN) } } catch (error) { logError(toError(error)) return [] } return []}
/** * Gets all release notes as an array of [version, notes] arrays. * Versions are sorted with oldest first. * * @param readChangelog - Function to read the changelog (defaults to readChangelogFile) * @returns Array of [version, notes[]] arrays */export function getAllReleaseNotes( changelogContent: string = getStoredChangelogFromMemory(),): Array<[string, string[]]> { try { const releaseNotes = parseChangelog(changelogContent)
// Sort versions with oldest first const sortedVersions = Object.keys(releaseNotes).sort((a, b) => gt(a, b) ? 1 : -1, )
// Return array of [version, notes] arrays return sortedVersions .map(version => { const versionNotes = releaseNotes[version] if (!versionNotes || versionNotes.length === 0) return null
const notes = versionNotes.filter(Boolean) if (notes.length === 0) return null
return [version, notes] as [string, string[]] }) .filter((item): item is [string, string[]] => item !== null) } catch (error) { logError(toError(error)) return [] }}
/** * Checks if there are release notes to show based on the last seen version. * Can be used by multiple components to determine whether to display release notes. * Also triggers a fetch of the latest changelog if the version has changed. * * @param lastSeenVersion The last version of release notes the user has seen * @param currentVersion The current application version, defaults to MACRO.VERSION * @returns An object with hasReleaseNotes and the releaseNotes content */export async function checkForReleaseNotes( lastSeenVersion: string | null | undefined, currentVersion: string = MACRO.VERSION,): Promise<{ hasReleaseNotes: boolean; releaseNotes: string[] }> { // For Ant builds, use VERSION_CHANGELOG bundled at build time if (process.env.USER_TYPE === 'ant') { const changelog = MACRO.VERSION_CHANGELOG if (changelog) { const commits = changelog.trim().split('\n').filter(Boolean) return { hasReleaseNotes: commits.length > 0, releaseNotes: commits, } } return { hasReleaseNotes: false, releaseNotes: [], } }
// Ensure the in-memory cache is populated for subsequent sync reads const cachedChangelog = await getStoredChangelog()
// If the version has changed or we don't have a cached changelog, fetch a new one // This happens in the background and doesn't block the UI if (lastSeenVersion !== currentVersion || !cachedChangelog) { fetchAndStoreChangelog().catch(error => logError(toError(error))) }
const releaseNotes = getRecentReleaseNotes( currentVersion, lastSeenVersion, cachedChangelog, ) const hasReleaseNotes = releaseNotes.length > 0
return { hasReleaseNotes, releaseNotes, }}
/** * Synchronous variant of checkForReleaseNotes for React render paths. * Reads only from the in-memory cache populated by the async version. * setup.ts awaits checkForReleaseNotes() before first render, so this * returns accurate results in component render bodies. */export function checkForReleaseNotesSync( lastSeenVersion: string | null | undefined, currentVersion: string = MACRO.VERSION,): { hasReleaseNotes: boolean; releaseNotes: string[] } { // For Ant builds, use VERSION_CHANGELOG bundled at build time if (process.env.USER_TYPE === 'ant') { const changelog = MACRO.VERSION_CHANGELOG if (changelog) { const commits = changelog.trim().split('\n').filter(Boolean) return { hasReleaseNotes: commits.length > 0, releaseNotes: commits, } } return { hasReleaseNotes: false, releaseNotes: [], } }
const releaseNotes = getRecentReleaseNotes(currentVersion, lastSeenVersion) return { hasReleaseNotes: releaseNotes.length > 0, releaseNotes, }}