Dashboard › cli › Distillation
3667b1f8-601b-45df-a82f-b24f758d9cf2["lore_tm_v1_HP6TeE_xtxVUZDQkVRB6hREZ-mYyZuJtjyMytlYkDG4","lore_tm_v1_fGCqt09dLgTZfd4zzKzbzVVzOOIz8U4rhtbSlvOWHC4","lore_tm_v1_OKAkyYVp5rDpo6mCivJaI3ccEjZe_GNmX-SNeZZEjsQ"]
packages/cli/src/lib/binary.ts:29-54 defines curl-install locations: KNOWN_CURL_DIRS = [".local/bin", "bin", ".sentry/bin"], LEGACY_INSTALL_SUBDIR = join(".sentry", "bin"), and LEGACY_INSTALL_SUBDIRS = [LEGACY_INSTALL_SUBDIR]. Migration is deliberately limited to pre-XDG ~/.sentry/bin; ~/.local/bin and ~/bin are valid current XDG targets and must not be relocated.stripTrailingSep() may remove a trailing path separator but never from a bare root such as /.packages/cli/src/lib/binary.ts:32-78 resolves IS_CASE_INSENSITIVE_FS once at module load for Windows and macOS. stripTrailingSep(p) strips one trailing separator only when p.length > 1; samePath(a, b) tolerates trailing separators and lowercases paths on case-insensitive filesystems, avoiding false differences such as C:\Users\User versus C:\Users\user.packages/cli/src/lib/binary.ts:84-85 defines getLegacyInstallDirs(homeDir) by joining homeDir with every entry in LEGACY_INSTALL_SUBDIRS.packages/cli/src/lib/binary.ts:96-130 defines InstallationMethod as "curl" | "brew" | "npm" | "pnpm" | "bun" | "yarn" | "unknown" and UpgradeSource with readonly githubRepo, ghcrRepo, and tagPrefix. Ordered UPGRADE_SOURCES are: 1. { githubRepo: "getsentry/toolkit", ghcrRepo: "getsentry/toolkit", tagPrefix: "cli@" }; 2. { githubRepo: "getsentry/cli", ghcrRepo: "getsentry/cli", tagPrefix: "" }. PRIMARY_UPGRADE_SOURCE is the first entry.packages/cli/src/lib/binary.ts:132-159 accepts explicit installation methods curl, brew, npm, pnpm, bun, and yarn. parseInstallationMethod(value) lowercases input and throws Invalid method: ${value}. Must be one of: ${VALID_METHODS.join(", ")} for anything else.packages/cli/src/lib/binary.ts:162-205 defines cached musl detection on Linux. Heuristic order: 1. check /lib/ld-musl-${muslArch}.so.1, with x86_64 for process.arch === "x64" and aarch64 otherwise; 2. run ldd --version, concatenate stdout and stderr, and test case-insensitively for "musl". Non-Linux returns false; failure to run ldd assumes glibc and caches false.packages/cli/src/lib/binary.ts:207-226 defines platform artifact names as sentry-<os>-<arch>[-musl][.exe]: OS is darwin, windows, or otherwise linux; architecture is arm64 only for process.arch === "arm64", otherwise x64; musl adds -musl; Windows adds .exe.packages/cli/src/lib/binary.ts:228-275 builds source-specific GitHub URLs. getBinaryDownloadUrl(version, source) returns https://github.com/${source.githubRepo}/releases/download/${source.tagPrefix}${version}/${getPlatformBinaryName()}; getGitHubReleasesUrl() targets /repos/${source.githubRepo}/releases; getGitHubReleaseByTagUrl() appends /tags/${encodeURIComponent(tag)}; getGitHubLatestReleaseUrl() uses ?per_page=100 when tagPrefix is nonempty and /latest otherwise; getGitHubRepositoryUrl() targets /repos/${source.githubRepo}; GITHUB_RELEASES_URL is the primary source’s releases URL.packages/cli/src/lib/binary.ts:277-347 defines ResolvedUpgradeSource as { source, response } and ResolveUpgradeSourceOptions with getProbeUrl, optional fetch, optional shared signal, and optional ordered sources. fetchUpgradeProbe() uses options.fetch ?? customFetch with getGitHubHeaders() and the signal; AbortError is rethrown unchanged, TLS certificate errors become UpgradeError("network_error", buildTlsErrorDetail(error)), and other connection failures become UpgradeError("network_error", "Failed to connect to GitHub: …").resolveUpgradeSource() in packages/cli/src/lib/binary.ts:320-347 probes options.sources ?? UPGRADE_SOURCES in order. A successful response returns { source, response }; only HTTP 404 falls through to the next source; every other HTTP status aborts with UpgradeError("network_error", "Failed to fetch from GitHub: HTTP …"); all-404 exhaustion throws UpgradeError("network_error", "No CLI upgrade source was found: every source returned HTTP 404").packages/cli/src/lib/binary.ts:349-382 recognizes nightlies by version.includes("-dev."), compares stable and nightly versions with semver.compare, and defines a downgrade as compareVersions(current, target) === 1. Nightly format is X.Y.Z-dev.<unix-seconds>, whose numeric prerelease identifier is compared numerically under SemVer.packages/cli/src/lib/binary.ts:384-413 returns sentry.exe on Windows and sentry elsewhere. getBinaryPaths(installPath) derives { installPath, tempPath: "${installPath}.download", oldPath: "${installPath}.old", lockPath: "${installPath}.lock" }.determineInstallDir(homeDir, env) in packages/cli/src/lib/binary.ts:415-459 uses this exact priority: 1. SENTRY_INSTALL_DIR; 2. absolute XDG_BIN_HOME only; 3. existing ~/.local/bin already in PATH; 4. existing ~/bin already in PATH; 5. fallback ~/.local/bin. PATH membership uses samePath() for trailing-separator and Windows/macOS casing tolerance.packages/cli/src/lib/binary.ts:461-503 defines GitHub headers as Accept: "application/vnd.github.v3+json" and "User-Agent": getUserAgent(). fetchWithUpgradeError(url, init, serviceName) uses customFetch, rethrows AbortError, maps TLS certificate failures through buildTlsErrorDetail(), and maps other failures to UpgradeError("network_error", "Failed to connect to ${serviceName}: …").replaceBinarySync(tempPath, installPath) in packages/cli/src/lib/binary.ts:505-541 is intentionally synchronous so the replacement sequence cannot be interrupted. Unix atomically renames temp over install. Windows first tries install→${installPath}.old; on failure it best-effort deletes .old and retries the first rename, tolerates another failure as a possible fresh install, then renames temp→install. Windows uses this alternative because a running executable can be renamed but not deleted or overwritten.cleanupOldBinary(oldPath) in packages/cli/src/lib/binary.ts:543-560 fire-and-forgets unlink(oldPath) and ignores errors. It deliberately does not remove .download files because another upgrade may be active; .download cleanup occurs inside the exclusively locked upgrade flow.acquireLock(lockPath) in packages/cli/src/lib/binary.ts:562-599 first runs mkdirSync(dirname(lockPath), { recursive: true, mode: 0o755 }), then atomically writes the current PID with { flag: "wx" }. Root cause addressed: fresh ~/.sentry/bin, npm→nightly migration, or a purged SENTRY_INSTALL_DIR could otherwise cause ENOENT ... open '.../sentry.lock' (CLI-1E1, CLI-1RV).mkdirSync() in acquireLock() is deliberately outside the lock-creation try/catch: directory failures such as EEXIST on a regular-file parent, ENOTDIR, or EACCES must propagate directly. Putting it inside would misroute EEXIST to handleExistingLock() and surface a misleading ENOTDIR.handleExistingLock(lockPath) in packages/cli/src/lib/binary.ts:601-648 reads and parses the owner PID. A read-time ENOENT recursively retries acquisition; other read errors propagate. A live non-parent PID throws UpgradeError("execution_failed", "Another upgrade is already in progress"); if the owner is process.ppid, ownership transfers by overwriting the lock with process.pid; a dead or invalid PID is stale, so the lock is removed and acquisition recursively retried. Removal tolerates only ENOENT.releaseLock(lockPath) in packages/cli/src/lib/binary.ts:650-662 synchronously unlinks the lock and ignores errors because it may already be gone.installBinary(sourcePath, installDir) in packages/cli/src/lib/binary.ts:664-700 creates installDir recursively with mode 0o755, derives the platform binary’s install/temp/lock paths, and acquires the PID lock. When an upgrade-spawned setup --install is already executing from the .download file, it avoids unlinking and copying that same file. Equality is checked using symlink-resolved paths because process.execPath is OS-canonicalized while installDir may traverse a symlink such as macOS /tmp→/private/tmp; canonicalization uses realpath() and falls back to resolve() only on ENOENT, logging other realpath failures before fallback behavior shown beyond line 700.packages/cli/src/lib/ghcr.ts:1-18 documents the GHCR OCI design: public nightly artifacts use anonymous token exchange; the nightly version is read from manifest annotation annotations.version, requiring a token exchange plus manifest fetch (2 HTTP requests); GHCR blob 307 redirects must be followed manually without forwarding the Authorization header because Azure Blob Storage otherwise returns 404.packages/cli/src/lib/ghcr.ts:25-32 configures GHCR_REQUEST_TIMEOUT = 10_000, GHCR_MAX_RETRIES = 1, and GHCR_BLOB_TIMEOUT = 30_000.isRetryableError() in packages/cli/src/lib/ghcr.ts:34-55 treats TimeoutError, AbortError, and messages containing timeout, econnreset, econnrefused, network, or fetch failed as transient. HTTP-level errors are not matched because callers handle responses separately.packages/cli/src/lib/ghcr.ts:57-77 builds request cancellation with AbortSignal.timeout(timeout) and, when provided, AbortSignal.any([timeoutSignal, externalSignal]). isExternalAbort() distinguishes caller cancellation by requiring externalSignal.aborted and error name AbortError.fetchWithRetry() in packages/cli/src/lib/ghcr.ts:79-132 uses customFetch, a default 10-second timeout, and up to 2 total attempts (attempt = 0 through GHCR_MAX_RETRIES = 1). Caller-triggered abort stops immediately; only retryable timeout/network failures retry; exhaustion throws UpgradeError("network_error", "${context}: ${lastError?.message ?? "unknown error"}"). Rationale: observed GHCR cold-start latency can jump from 126ms to 30s; short timeout plus retry keeps the documented worst case near 20s.packages/cli/src/lib/ghcr.ts:134-180 sets GHCR_REPO = PRIMARY_UPGRADE_SOURCE.ghcrRepo, GHCR_TAG = "nightly", registry base https://ghcr.io, and OCI media type application/vnd.oci.image.manifest.v1+json. OciLayer includes digest, mediaType, byte size, and optional annotations; OciManifest includes schemaVersion, optional mediaType, optional config, layers, and optional manifest annotations.getAnonymousToken() in packages/cli/src/lib/ghcr.ts:182-224 supports either an UpgradeSource plus optional signal or a legacy/direct AbortSignal. It requests https://ghcr.io/token?scope=repository:${source.ghcrRepo}:pull with the CLI User-Agent; non-OK responses throw GHCR token exchange failed: HTTP …, missing token throws GHCR token exchange returned no token, and success returns the anonymous bearer token.fetchManifest() in packages/cli/src/lib/ghcr.ts:226-262 GETs https://ghcr.io/v2/${source.ghcrRepo}/manifests/${tag} with Authorization: Bearer ${token}, the OCI manifest Accept header, and CLI User-Agent. Non-OK responses throw UpgradeError("network_error", "Failed to fetch manifest for tag \"${tag}\": HTTP …"); success parses OciManifest. fetchNightlyManifest() delegates with GHCR_TAG.getNightlyVersion(manifest) in packages/cli/src/lib/ghcr.ts:281-299 returns manifest.annotations?.version; absence throws UpgradeError("network_error", "Nightly manifest has no version annotation").findLayerByFilename(manifest, filename) in packages/cli/src/lib/ghcr.ts:301-326 matches layer.annotations?.["org.opencontainers.image.title"] === filename; absence throws UpgradeError("version_not_found", "No nightly build found for ${filename}").downloadNightlyBlob() in packages/cli/src/lib/ghcr.ts:328-425 first requests https://ghcr.io/v2/${source.ghcrRepo}/blobs/${digest} with bearer auth, User-Agent, redirect: "manual", and a 30-second GHCR-side signal. A direct 200 is returned. Redirect statuses 301, 302, 307, and 308 require a Location header and are followed with only the User-Agent—never the bearer token.downloadNightlyBlob() uses only the caller’s signal and intentionally has no AbortSignal.timeout: the timeout would cover connection plus body streaming, and a full nightly binary of about 30 MB would need sustained throughput around 8 Mbps to finish within 30 seconds. GHCR step 1 retains its timeout; Azure is treated as having reliable latency.downloadNightlyBlob() error mappings are: initial transport failure → UpgradeError("network_error", "Failed to connect to GHCR: …"); redirect without Location → GHCR blob redirect (${status}) had no Location header; blob-storage transport failure → Failed to download from blob storage: …; non-OK blob response → Blob storage download failed: HTTP …; any other initial status → Unexpected GHCR blob response: HTTP ….packages/cli/src/lib/ghcr.ts:427-512 paginates GHCR tags with TAGS_PAGE_SIZE = 100. fetchTagPage() requests /v2/${source.ghcrRepo}/tags/list?n=100, adding &last=${encodeURIComponent(lastTag)} after the first page, and returns data.tags ?? []; non-OK responses throw UpgradeError("network_error", "Failed to list GHCR tags: HTTP …").listTags(token, prefix?, signal?, source?) repeatedly calls fetchTagPage(), preserves only tags starting with prefix when supplied, stops on an empty page or any page shorter than 100 entries, and otherwise advances with lastTag = tags.at(-1). The registry’s lexicographic ordering is preserved.downloadLayerBlob() in packages/cli/src/lib/ghcr.ts:514-534 reuses downloadNightlyBlob(token, digest, signal, source) and fully buffers response.arrayBuffer(). It is intended for small OCI patch payloads of 50-500 KB, unlike streaming full nightly binaries.