Dashboard › cli › Distillation
32a49d39-1070-4955-bfae-966d4586f1f3["lore_tm_v1_E0413f5KzQpplG446YmEK12NWO2J4PaK6zZatvt-G6Y","lore_tm_v1_ffaKSdo3UE2brM2HEJkT9LrRAwmYpq16KNCEYcW9bfQ"]
detectInstallationMethod() in packages/cli/src/lib/upgrade.ts must always check for Homebrew first because stored install info may be stale; Homebrew’s resolved /Cellar/ path is treated as cheap and authoritative.detectInstallationMethod() priority is: 1. Homebrew via isHomebrewInstall(), 2. stored DB install info, 3. legacy detection via curl paths → package-manager subprocesses → node_modules path, 4. best-effort persistence with setInstallInfo({ method: legacyMethod, path: process.execPath, version: CLI_VERSION }).isHomebrewInstall() resolves process.execPath with realpathSync() before checking for "/Cellar/"; if resolution throws because the binary was deleted or moved, it checks the unresolved path.getCurlInstallPaths() trusts getInstallInfo().path only when stored.method === "curl" and existsSync(dirname(stored.path)); this prevents a purged SENTRY_INSTALL_DIR such as /tmp/sentry-test-install from causing ENOENT ... open '.../sentry.lock'. It then checks process.execPath against lazily computed known curl paths and finally falls back to join(homedir(), ".sentry", "bin", getBinaryFilename()).getKnownCurlPaths() lazily computes KNOWN_CURL_DIRS.map((dir) => join(homedir(), dir) + sep) to avoid TDZ problems from circular imports and preserve directory-boundary matching.startCleanupOldBinary() obtains oldPath from getCurlInstallPaths() and calls cleanupOldBinary(oldPath) as fire-and-forget startup cleanup.runCommand(command, args) uses spawn() with stdio: ["ignore", "pipe", "pipe"] and shell: process.platform === "win32" so Windows .cmd package-manager executables work; it collects trimmed stdout, drains discarded stderr with proc.stderr.resume(), maps a null close code to 1, and rejects on process error.isInstalledWith(pm) runs Yarn as yarn global list --depth=0; npm, pnpm, and Bun use [pm] list -g sentry. Detection succeeds only when exit code is 0 and stdout contains "sentry@"; errors return false.detectPackageManagerFromPath() inspects process.argv[1], requires a NODE_MODULES_DIRNAME path segment, maps a ".pnpm" segment to "pnpm", a ".bun" segment to "bun", and all other node_modules layouts—including npm and Yarn Classic—to "npm".detectLegacyInstallationMethod() checks known curl paths first, then package managers in exact order ["npm", "pnpm", "bun", "yarn"], then detectPackageManagerFromPath(), and finally returns "unknown"."Failed to persist install info (DB may be read-only)".${GITHUB_RELEASES_URL}/latest and package-manager installs to ${NPM_REGISTRY_URL}/latest; GitHub strips VERSION_PREFIX_REGEX from tag_name, while npm returns version.fetchLatestNightlyVersion(signal) performs exactly 2 HTTP requests—anonymous GHCR token exchange and rolling-nightly manifest fetch—and extracts the manifest’s annotations.version; it checks signal?.aborted before each network call and throws AbortError.fetchLatestVersion(method, channel = "stable") sends "nightly" to fetchLatestNightlyVersion(), stable "curl"/"brew" to fetchLatestFromGitHub(), and other stable methods to fetchLatestFromNpm().nightlyVersionExists(version) fetches GHCR tag nightly-${version}; HTTP 404 or 403 represented in an UpgradeError returns false, while other errors propagate.versionExists(method, version) always checks nightly versions in GHCR. Stable curl/Homebrew versions use a HEAD request to ${GITHUB_RELEASES_URL}/tags/${version} with GitHub headers; stable package-manager versions use HEAD on ${NPM_REGISTRY_URL}/${version}.DownloadResult in packages/cli/src/lib/upgrade.ts has fields tempBinaryPath: string, lockPath: string, and optional patchBytes?: number.streamDecompressToFile() pipes a ReadableStream<Uint8Array> through new DecompressionStream("gzip"), writes via createWriteStream(destPath), and reports indeterminate decompressed bytes through makeByteProgress("Downloading", null, setMessage).streamDecompressToFile() must always be flushed/closed, including when streaming fails.streamDecompressToFile() installs an early writer "error" listener and retains the first failure in writeError to prevent an unhandled ERR_UNHANDLED_ERROR for errors such as ENOSPC or EIO.ENOSPC while the write buffer is full may never emit "drain"; backpressure handling therefore races one-time "drain" and "error" listeners and removes the unused listener to avoid hangs and MaxListenersExceededWarning.streamDecompressToFile() stores the original iteration failure in streamError, always calls progress.done() in finally, then awaits writer.end(). If writer.end() fails after a stream failure, it logs writer.end failed after a stream error: ${String(endErr)} and rethrows the original stream error so cleanup cannot mask the root cause; without a prior stream error, the end error is thrown.https://github.com/oven-sh/bun/issues/13237, where streaming response bodies can be garbage-collected or the process can exit before completion.getNightlyGzFilename() returns ${getPlatformBinaryName()}.gz; nightly OCI layers use filenames such as sentry-<os>-<arch>.gz or sentry-windows-x64.exe.gz.downloadNightlyToPath(destPath, version?, setMessage?) gets an anonymous token, fetches either nightly-${version} or the rolling :nightly manifest, locates the platform .gz layer with findLayerByFilename(), downloads by digest with downloadNightlyBlob(), rejects a missing body with UpgradeError("execution_failed", "GHCR blob response had no body"), and streams decompression to disk.downloadStableToPath() first tries ${getBinaryDownloadUrl(version)}.gz—described as approximately 37 MB versus 99 MB, or about 60% smaller—and streams decompression; any compressed-path failure falls back to the raw URL. The raw response is fully buffered with response.arrayBuffer() before writeFile(destPath, new Uint8Array(body)).waitForBinaryVisible(path) polls probeBinaryFile() for a regular, non-empty file using 6 probes and exponential delays of 100 ms, 200 ms, 400 ms, 800 ms, and 1600 ms; probe times are 0 ms, 100 ms, 300 ms, 700 ms, 1500 ms, and 3100 ms.waitForBinaryVisible() must fail when the downloaded binary never becomes visible or stays empty.1.3.9 issue CLI-1D3, where writer.end() may return before the OS exposes the file and a subsequent Bun.spawn fails with "Executable not found in $PATH".waitForBinaryVisible() throws UpgradeError("execution_failed", \Downloaded binary is missing or empty at ${path}. This is usually transient — rerun `sentry cli upgrade` to retry.`)`; if visibility required multiple attempts, it logs Binary became visible after ${attempt} attempts.downloadBinaryToTemp() acquires lockPath, removes a leftover tempPath, tries tryDeltaUpgrade(version, tempPath, !!offline, setMessage) first, preserves deltaResult.patchBytes, and falls back to downloadFullBinary() when online.UpgradeError("offline_cache_miss", ...) telling the user to rerun sentry cli upgrade without --offline; a "network-fallback" miss instead reports that the network is unavailable and no pre-downloaded update was found.packages/cli/src/lib/ghcr.ts uses GHCR request timeout GHCR_REQUEST_TIMEOUT = 10_000, blob timeout GHCR_BLOB_TIMEOUT = 30_000, and GHCR_MAX_RETRIES = 1, meaning at most 2 attempts."TimeoutError" and "AbortError" and messages containing "timeout", "econnreset", "econnrefused", "network", or "fetch failed"; HTTP errors are not retried by fetchWithRetry().buildSignal(timeout, externalSignal?) combines AbortSignal.timeout(timeout) with a caller signal via AbortSignal.any(...); fetchWithRetry() immediately stops retrying when isExternalAbort() sees the external signal aborted with error name "AbortError".GHCR_REPO = "getsentry/cli", GHCR_TAG = "nightly", registry "https://ghcr.io", and OCI manifest media type "application/vnd.oci.image.manifest.v1+json".OciLayer fields are digest: string, mediaType: string, size: number, and optional annotations?: Record<string, string>; OciManifest fields are schemaVersion: number, optional mediaType, optional config, layers: OciLayer[], and optional manifest-level annotations.getAnonymousToken(signal?) requests ${GHCR_REGISTRY}/token?scope=repository:${GHCR_REPO}:pull with getUserAgent(), rejects non-OK responses as GHCR token exchange failed: HTTP ${response.status}, and rejects a missing token as "GHCR token exchange returned no token".fetchManifest(token, tag, signal?) requests ${GHCR_REGISTRY}/v2/${GHCR_REPO}/manifests/${tag} with Bearer authorization, Accept: OCI_MANIFEST_TYPE, and getUserAgent(); fetchNightlyManifest(token) wraps it with GHCR_TAG.getNightlyVersion(manifest) reads manifest.annotations?.version and throws UpgradeError("network_error", "Nightly manifest has no version annotation") if absent.findLayerByFilename() matches layer.annotations?.["org.opencontainers.image.title"]; absence throws UpgradeError("version_not_found", \No nightly build found for ${filename}`)`.Authorization header to Azure Blob Storage causes HTTP 404: step 1 requests the GHCR blob with redirect: "manual" and authorization; step 2 follows status 301, 302, 307, or 308 using only "User-Agent" and no authorization.downloadNightlyBlob() accepts a direct GHCR 200; a redirect without Location throws GHCR blob redirect (${blobResponse.status}) had no Location header; a failed Azure response throws Blob storage download failed: HTTP ${redirectResponse.status}; any other GHCR status throws Unexpected GHCR blob response: HTTP ${blobResponse.status}.30_000 ms timeout, but the redirected Azure download intentionally has no AbortSignal.timeout: a full nightly binary is approximately 30 MB, and a 30 s timeout would require sustained throughput of approximately 8 Mbps; only the caller-provided signal is passed.TAGS_PAGE_SIZE = 100 and endpoint ${GHCR_REGISTRY}/v2/${GHCR_REPO}/tags/list?n=${TAGS_PAGE_SIZE}, adding &last=${encodeURIComponent(lastTag)} for subsequent pages.listTags(token, prefix?, signal?) accumulates optionally prefix-filtered tags until a page is empty or contains fewer than 100 tags; for a full page it advances with lastTag = tags.at(-1).downloadLayerBlob(token, digest, signal?) reuses downloadNightlyBlob()’s redirect-without-auth behavior and returns response.arrayBuffer(); it is intended for small patch payloads of 50-500 KB.