diff --git a/src/commands/cli/upgrade.ts b/src/commands/cli/upgrade.ts index 6a5ab47e5..7fb4599ff 100644 --- a/src/commands/cli/upgrade.ts +++ b/src/commands/cli/upgrade.ts @@ -32,6 +32,7 @@ import { import { getVersionCheckInfo } from "../../lib/db/version-check.js"; import { UpgradeError } from "../../lib/errors.js"; import { formatUpgradeResult } from "../../lib/formatters/human.js"; +import { formatBytes } from "../../lib/formatters/numbers.js"; import { CommandOutput } from "../../lib/formatters/output.js"; import { logger } from "../../lib/logger.js"; import { withProgress } from "../../lib/polling.js"; @@ -541,8 +542,9 @@ async function executeStandardUpgrade(opts: { ); if (downloadResult?.patchBytes) { - const kb = (downloadResult.patchBytes / 1024).toFixed(1); - log.info(`Applied delta patch (${kb} KB downloaded)`); + log.info( + `Applied delta patch (${formatBytes(downloadResult.patchBytes)} downloaded)` + ); } // Run setup on the new binary to update completions, agent skills, @@ -611,8 +613,9 @@ async function migrateToStandaloneForNightly( ); if (downloadResult?.patchBytes) { - const kb = (downloadResult.patchBytes / 1024).toFixed(1); - log.info(`Applied delta patch (${kb} KB downloaded)`); + log.info( + `Applied delta patch (${formatBytes(downloadResult.patchBytes)} downloaded)` + ); } if (!downloadResult) { diff --git a/src/lib/delta-upgrade.ts b/src/lib/delta-upgrade.ts index 866148156..8aece148a 100644 --- a/src/lib/delta-upgrade.ts +++ b/src/lib/delta-upgrade.ts @@ -30,6 +30,7 @@ import { } from "./binary.js"; import { applyPatch } from "./bspatch.js"; import { CLI_VERSION } from "./constants.js"; +import { formatBytes } from "./formatters/numbers.js"; import { downloadLayerBlob, fetchManifest, @@ -1017,7 +1018,7 @@ async function resolveAndApplyDelta( if (cached) { Sentry.getActiveSpan()?.setAttribute("delta.source", "cache"); log.debug( - `Using cached patches: ${cached.patches.length} patch(es), ${cached.totalSize} bytes total` + `Using cached patches: ${cached.patches.length} patch(es), ${formatBytes(cached.totalSize)} total` ); return await applyChainAndReturn(cached, oldBinaryPath, destPath); } @@ -1034,7 +1035,7 @@ async function resolveAndApplyDelta( const chain = await resolveFromNetwork(); if (chain) { log.debug( - `Resolved ${channel} chain: ${chain.patches.length} patch(es), ${chain.totalSize} bytes total` + `Resolved ${channel} chain: ${chain.patches.length} patch(es), ${formatBytes(chain.totalSize)} total` ); } if (!chain) { diff --git a/src/lib/formatters/numbers.ts b/src/lib/formatters/numbers.ts index aa8b12d72..7aff44b62 100644 --- a/src/lib/formatters/numbers.ts +++ b/src/lib/formatters/numbers.ts @@ -99,6 +99,33 @@ export function formatCompactWithUnit( return appendUnitSuffix(compactFormatter.format(Math.round(value)), unit); } +/** + * Format a byte count as a human-readable string with binary units. + * + * Uses 1024-based conversion with one fractional digit. Values below 1 KB + * are shown as whole bytes. Unlike {@link formatCompactWithUnit} with + * `"byte"`, this avoids the "BB" collision where `Intl.NumberFormat` + * compact notation uses "B" for billions and `appendUnitSuffix` adds + * another "B" for bytes. + * + * @example formatBytes(512) // "512 B" + * @example formatBytes(3435689) // "3.3 MB" + * @example formatBytes(106989888) // "102.0 MB" + * @example formatBytes(1073741824) // "1.0 GB" + */ +export function formatBytes(bytes: number): string { + if (bytes < 1024) { + return `${bytes} B`; + } + if (bytes < 1024 * 1024) { + return `${(bytes / 1024).toFixed(1)} KB`; + } + if (bytes < 1024 * 1024 * 1024) { + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; + } + return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`; +} + /** * Format a percentage value with one decimal place, or "—" when absent. * diff --git a/src/lib/upgrade.ts b/src/lib/upgrade.ts index 7ec2295fa..9758b9acf 100644 --- a/src/lib/upgrade.ts +++ b/src/lib/upgrade.ts @@ -29,6 +29,7 @@ import { getInstallInfo, setInstallInfo } from "./db/install-info.js"; import type { ReleaseChannel } from "./db/release-channel.js"; import { attemptDeltaUpgrade, type DeltaResult } from "./delta-upgrade.js"; import { AbortError, UpgradeError } from "./errors.js"; +import { formatBytes } from "./formatters/numbers.js"; import { downloadNightlyBlob, fetchManifest, @@ -804,7 +805,7 @@ export async function downloadBinaryToTemp( // exponential backoff so a transient filesystem-visibility race // self-heals without asking the user to rerun. const verifiedSize = await waitForBinaryVisible(tempPath); - log.debug(`Downloaded binary verified (${verifiedSize} bytes)`); + log.debug(`Binary verified (${formatBytes(verifiedSize)})`); // Clear consumed patch cache — patches for the old version are useless // after the binary has been updated (whether via delta or full download).