@tryaura/aura-cli 0.3.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,17 +2,17 @@ import { A as displayPath, C as isRecord$4, D as SHARED_INSTRUCTIONS_TEMPLATE, E
2
2
  import { COMMAND_NOT_FOUND_EXIT_CODE, DEFAULT_EXEC_TIMEOUT_MS, DEFAULT_HTTP_TIMEOUT_MS, MAX_EXEC_OUTPUT_CHARACTERS, MAX_EXEC_TIMEOUT_MS, MAX_HTTP_RESPONSE_BYTES, MAX_HTTP_TIMEOUT_MS, McpWriteError, NOT_EXECUTABLE_EXIT_CODE, OUTPUT_LIMIT_EXIT_CODE, SHARED_INSTRUCTIONS_TEMPLATE_TOKEN, TIMEOUT_EXIT_CODE, defineOwnProperty, detectExecutable, hasMcpRedaction, jsonPropertyPath, mcpEnvironmentVariableNames, mcpServerNameProblem, normalizeMcpServerDefinition, parseMcpServerDefinition, parseMcpServerManifest, parseMcpServerManifestValue, parseSkillFrontmatter, parseSkillReferences, resolveMcpSecretNameCollisions, resolveSkillDirectory, splitSourceLines } from "@tryaura/aura-sdk";
3
3
  import { basename, delimiter, dirname, isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
4
4
  import { Buffer as Buffer$1, isUtf8 } from "node:buffer";
5
- import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
5
+ import { createHash, createPublicKey, randomUUID, timingSafeEqual, verify } from "node:crypto";
6
6
  import { constants, homedir } from "node:os";
7
- import { spawn } from "node:child_process";
8
- import { constants as constants$1 } from "node:fs";
9
- import { access, chmod, link, lstat, mkdir, open, opendir, readFile, readdir, readlink, realpath, rename, rm, rmdir, stat, symlink, unlink, utimes, writeFile } from "node:fs/promises";
7
+ import { execFile, spawn } from "node:child_process";
8
+ import { constants as constants$1, createReadStream } from "node:fs";
9
+ import { access, chmod, copyFile, link, lstat, mkdir, open, opendir, readFile, readdir, readlink, realpath, rename, rm, rmdir, stat, symlink, unlink, utimes, writeFile } from "node:fs/promises";
10
10
  import { applyPatch, createTwoFilesPatch, structuredPatch } from "diff";
11
- import { coerce, parse, satisfies, valid, validRange } from "semver";
11
+ import { coerce, gt, parse, satisfies, valid, validRange } from "semver";
12
12
  import { isDeepStrictEqual } from "node:util";
13
13
  import { fileURLToPath, pathToFileURL } from "node:url";
14
14
  import { EnvHttpProxyAgent, fetch as fetch$1 } from "undici";
15
- import { gunzipSync } from "node:zlib";
15
+ import { createGunzip, gunzipSync } from "node:zlib";
16
16
  import process$1 from "node:process";
17
17
  import { Builtins, Cli, Command, Option } from "clipanion/lib/advanced/index.js";
18
18
  import { HelpCommand } from "clipanion/lib/advanced/HelpCommand.js";
@@ -1474,13 +1474,13 @@ async function applyPreparedOperation(prepared, plan, registerUndo, filesystem =
1474
1474
  }
1475
1475
  //#endregion
1476
1476
  //#region ../core/src/fix-plan/journal-lock-record.ts
1477
- const LOCK_STALE_MS = 3e5;
1477
+ const LOCK_STALE_MS$1 = 3e5;
1478
1478
  const UNREADABLE_LOCK_GRACE_MS = 5e3;
1479
1479
  async function inspectLock(path, now) {
1480
1480
  try {
1481
1481
  const owner = parseOwner(await readFile(path, "utf8"));
1482
1482
  if (owner === void 0) return await unreadableLockIsStale(path, now) ? { kind: "stale" } : { kind: "active" };
1483
- if (now().getTime() - owner.acquiredAtMs <= LOCK_STALE_MS || processIsAlive(owner.pid)) return {
1483
+ if (now().getTime() - owner.acquiredAtMs <= LOCK_STALE_MS$1 || processIsAlive(owner.pid)) return {
1484
1484
  kind: "active",
1485
1485
  owner
1486
1486
  };
@@ -5786,7 +5786,7 @@ const MAX_CAUSE_DEPTH = 8;
5786
5786
  const MAX_DETAIL_CHARACTERS = 300;
5787
5787
  const MAX_REDIRECTS = 3;
5788
5788
  const URL_PROBE_TIMEOUT_MS = 3e3;
5789
- const REDIRECT_STATUSES = /* @__PURE__ */ new Set([
5789
+ const REDIRECT_STATUSES$1 = /* @__PURE__ */ new Set([
5790
5790
  301,
5791
5791
  302,
5792
5792
  303,
@@ -5833,13 +5833,13 @@ async function probeMcpUrl(url, request) {
5833
5833
  }
5834
5834
  async function runProbe(url, signal, request) {
5835
5835
  const redirects = { count: 0 };
5836
- const head = await follow(url, "HEAD", signal, redirects, request);
5836
+ const head = await follow$1(url, "HEAD", signal, redirects, request);
5837
5837
  if (head.kind !== "response") return outcomeProblem(head);
5838
5838
  if (head.response.status !== 405) return responseProblem(head.response.status);
5839
- const get = await follow(head.url, "GET", signal, redirects, request);
5839
+ const get = await follow$1(head.url, "GET", signal, redirects, request);
5840
5840
  return get.kind === "response" ? responseProblem(get.response.status) : outcomeProblem(get);
5841
5841
  }
5842
- async function follow(initialUrl, method, signal, redirects, request) {
5842
+ async function follow$1(initialUrl, method, signal, redirects, request) {
5843
5843
  let url = initialUrl;
5844
5844
  while (true) {
5845
5845
  const response = await request({
@@ -5847,7 +5847,7 @@ async function follow(initialUrl, method, signal, redirects, request) {
5847
5847
  signal,
5848
5848
  url
5849
5849
  });
5850
- if (!REDIRECT_STATUSES.has(response.status)) return {
5850
+ if (!REDIRECT_STATUSES$1.has(response.status)) return {
5851
5851
  kind: "response",
5852
5852
  response,
5853
5853
  url
@@ -6708,6 +6708,72 @@ function environmentValue(variables, name) {
6708
6708
  return variables[name] ?? variables[name.toUpperCase()];
6709
6709
  }
6710
6710
  //#endregion
6711
+ //#region ../core/src/workspace/disk-cache.ts
6712
+ /**
6713
+ * Shared primitives for the on-disk caches below `~/agents/.cache`.
6714
+ *
6715
+ * A cache here is an optimization and nothing more: every reader treats a malformed, oversized,
6716
+ * or unreadable entry as a miss, and a failed write leaves the world exactly as it was. Entries
6717
+ * are private to the user (`0700` directories, `0600` files) and land atomically via a temporary
6718
+ * file and rename, so a concurrent run never observes half an entry.
6719
+ */
6720
+ /**
6721
+ * The caches' own reader, built once rather than per lookup.
6722
+ *
6723
+ * Deliberately not a caller-injected reader: entries are written here with `node:fs` and are
6724
+ * machine state rather than workspace content, so reading them through a substituted reader would
6725
+ * let a run see a cache it cannot write and write one it cannot see.
6726
+ */
6727
+ const CACHE_READER = createFileReader();
6728
+ /**
6729
+ * Resolves one entry's location below `~/agents/.cache/<namespace>/<sha256(key)>`.
6730
+ *
6731
+ * Takes the home directory rather than a whole {@link Environment} so a caller that runs before
6732
+ * one exists — the startup updater, which decides whether to check for a release at all — reaches
6733
+ * the same directory layout as every other cache instead of inventing a second one.
6734
+ */
6735
+ function cacheLocation(environment, namespace, key) {
6736
+ const directory = join(environment.homeDir, "agents", ".cache", namespace);
6737
+ const hash = createHash("sha256").update(key, "utf8").digest("hex");
6738
+ return {
6739
+ directory,
6740
+ path: join(directory, hash)
6741
+ };
6742
+ }
6743
+ /** Reads one entry's JSON envelope, or `undefined` for any reason it cannot be used. */
6744
+ async function readCacheEnvelope(location, maxBytes) {
6745
+ try {
6746
+ const contents = await CACHE_READER.read(location.path, { maxBytes });
6747
+ if (!contents.exists || contents.problem !== void 0 || contents.content === void 0) return;
6748
+ const value = JSON.parse(contents.content);
6749
+ return isPlainRecord$2(value) ? value : void 0;
6750
+ } catch {
6751
+ return;
6752
+ }
6753
+ }
6754
+ /** Stores one entry's envelope, treating any write failure as nothing having happened. */
6755
+ async function writeCacheEnvelope(location, envelope) {
6756
+ const temporary = join(location.directory, `.${randomUUID()}.tmp`);
6757
+ try {
6758
+ await mkdir(location.directory, {
6759
+ mode: 448,
6760
+ recursive: true
6761
+ });
6762
+ await writeFile(temporary, `${JSON.stringify(envelope)}\n`, {
6763
+ encoding: "utf8",
6764
+ mode: 384
6765
+ });
6766
+ await rename(temporary, location.path);
6767
+ } catch {
6768
+ try {
6769
+ await unlink(temporary);
6770
+ } catch {}
6771
+ }
6772
+ }
6773
+ function isPlainRecord$2(value) {
6774
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6775
+ }
6776
+ //#endregion
6711
6777
  //#region ../core/src/skills/agenticskills-http.ts
6712
6778
  /**
6713
6779
  * Performs one bounded GET, retrying once on a transient failure unless the caller opts out.
@@ -6833,66 +6899,6 @@ function daysInMonth(year, month) {
6833
6899
  return month >= 1 && month <= 12 ? 31 : void 0;
6834
6900
  }
6835
6901
  //#endregion
6836
- //#region ../core/src/workspace/disk-cache.ts
6837
- /**
6838
- * Shared primitives for the on-disk caches below `~/agents/.cache`.
6839
- *
6840
- * A cache here is an optimization and nothing more: every reader treats a malformed, oversized,
6841
- * or unreadable entry as a miss, and a failed write leaves the world exactly as it was. Entries
6842
- * are private to the user (`0700` directories, `0600` files) and land atomically via a temporary
6843
- * file and rename, so a concurrent run never observes half an entry.
6844
- */
6845
- /**
6846
- * The caches' own reader, built once rather than per lookup.
6847
- *
6848
- * Deliberately not a caller-injected reader: entries are written here with `node:fs` and are
6849
- * machine state rather than workspace content, so reading them through a substituted reader would
6850
- * let a run see a cache it cannot write and write one it cannot see.
6851
- */
6852
- const CACHE_READER = createFileReader();
6853
- /** Resolves one entry's location below `~/agents/.cache/<namespace>/<sha256(key)>`. */
6854
- function cacheLocation(environment, namespace, key) {
6855
- const directory = join(environment.homeDir, "agents", ".cache", namespace);
6856
- const hash = createHash("sha256").update(key, "utf8").digest("hex");
6857
- return {
6858
- directory,
6859
- path: join(directory, hash)
6860
- };
6861
- }
6862
- /** Reads one entry's JSON envelope, or `undefined` for any reason it cannot be used. */
6863
- async function readCacheEnvelope(location, maxBytes) {
6864
- try {
6865
- const contents = await CACHE_READER.read(location.path, { maxBytes });
6866
- if (!contents.exists || contents.problem !== void 0 || contents.content === void 0) return;
6867
- const value = JSON.parse(contents.content);
6868
- return isPlainRecord$1(value) ? value : void 0;
6869
- } catch {
6870
- return;
6871
- }
6872
- }
6873
- /** Stores one entry's envelope, treating any write failure as nothing having happened. */
6874
- async function writeCacheEnvelope(location, envelope) {
6875
- const temporary = join(location.directory, `.${randomUUID()}.tmp`);
6876
- try {
6877
- await mkdir(location.directory, {
6878
- mode: 448,
6879
- recursive: true
6880
- });
6881
- await writeFile(temporary, `${JSON.stringify(envelope)}\n`, {
6882
- encoding: "utf8",
6883
- mode: 384
6884
- });
6885
- await rename(temporary, location.path);
6886
- } catch {
6887
- try {
6888
- await unlink(temporary);
6889
- } catch {}
6890
- }
6891
- }
6892
- function isPlainRecord$1(value) {
6893
- return typeof value === "object" && value !== null && !Array.isArray(value);
6894
- }
6895
- //#endregion
6896
6902
  //#region ../core/src/skills/catalog-cache.ts
6897
6903
  /** How long a cached catalog serves with no network request at all. */
6898
6904
  const CATALOG_CACHE_FRESH_MS = 36e5;
@@ -6904,8 +6910,8 @@ const CATALOG_CACHE_FRESH_MS = 36e5;
6904
6910
  */
6905
6911
  const CATALOG_CACHE_MAX_AGE_MS = 6048e5;
6906
6912
  /** Doubled: the body is JSON stored inside JSON, so every quote costs one escape byte. */
6907
- const MAX_CACHE_BYTES$1 = 8004096;
6908
- const NAMESPACE$1 = "skill-catalogs";
6913
+ const MAX_CACHE_BYTES$2 = 8004096;
6914
+ const NAMESPACE$2 = "skill-catalogs";
6909
6915
  /**
6910
6916
  * Returns the cached document for one endpoint, or `undefined` for any reason it cannot be used.
6911
6917
  *
@@ -6913,7 +6919,7 @@ const NAMESPACE$1 = "skill-catalogs";
6913
6919
  * expired entry must never be the reason a run cannot list a catalog it could otherwise fetch.
6914
6920
  */
6915
6921
  async function readCatalogCache(environment, endpoint) {
6916
- const value = await readCacheEnvelope(cacheLocation(environment, NAMESPACE$1, endpoint), MAX_CACHE_BYTES$1);
6922
+ const value = await readCacheEnvelope(cacheLocation(environment, NAMESPACE$2, endpoint), MAX_CACHE_BYTES$2);
6917
6923
  if (value === void 0) return;
6918
6924
  const body = value["body"];
6919
6925
  const cachedAt = value["cachedAt"];
@@ -6929,7 +6935,7 @@ async function readCatalogCache(environment, endpoint) {
6929
6935
  }
6930
6936
  /** Stores one catalog body, treating any write failure as nothing having happened. */
6931
6937
  async function writeCatalogCache(environment, endpoint, body, etag) {
6932
- await writeCacheEnvelope(cacheLocation(environment, NAMESPACE$1, endpoint), {
6938
+ await writeCacheEnvelope(cacheLocation(environment, NAMESPACE$2, endpoint), {
6933
6939
  body,
6934
6940
  cachedAt: environment.now().getTime(),
6935
6941
  endpoint,
@@ -8584,8 +8590,8 @@ function isJsonArray(value) {
8584
8590
  //#endregion
8585
8591
  //#region ../core/src/preset/cache.ts
8586
8592
  const CACHE_TTL_MS = 864e5;
8587
- const MAX_CACHE_BYTES = 260096;
8588
- const NAMESPACE = "presets";
8593
+ const MAX_CACHE_BYTES$1 = 260096;
8594
+ const NAMESPACE$1 = "presets";
8589
8595
  /**
8590
8596
  * Returns the cached document for one reference, or `undefined` for any reason it cannot be used.
8591
8597
  *
@@ -8595,7 +8601,7 @@ const NAMESPACE = "presets";
8595
8601
  * so validating here would only duplicate that walk.
8596
8602
  */
8597
8603
  async function readPresetCache(environment, reference) {
8598
- const value = await readCacheEnvelope(cacheLocation(environment, NAMESPACE, reference), MAX_CACHE_BYTES);
8604
+ const value = await readCacheEnvelope(cacheLocation(environment, NAMESPACE$1, reference), MAX_CACHE_BYTES$1);
8599
8605
  if (value === void 0) return;
8600
8606
  const cachedAt = value["cachedAt"];
8601
8607
  const preset = value["preset"];
@@ -8605,7 +8611,7 @@ async function readPresetCache(environment, reference) {
8605
8611
  }
8606
8612
  /** Stores one validated document, treating any write failure as nothing having happened. */
8607
8613
  async function writePresetCache(environment, reference, preset) {
8608
- await writeCacheEnvelope(cacheLocation(environment, NAMESPACE, reference), {
8614
+ await writeCacheEnvelope(cacheLocation(environment, NAMESPACE$1, reference), {
8609
8615
  cachedAt: environment.now().getTime(),
8610
8616
  preset,
8611
8617
  reference
@@ -8671,7 +8677,7 @@ function extractPresetJson(compressed) {
8671
8677
  const paths = /* @__PURE__ */ new Set();
8672
8678
  while (offset + BLOCK <= archive.byteLength) {
8673
8679
  const header = archive.subarray(offset, offset + BLOCK);
8674
- if (isZeroBlock(header)) break;
8680
+ if (isZeroBlock$1(header)) break;
8675
8681
  if (!validTarChecksum(header)) return invalid$1("npm preset tarball has an invalid header checksum.");
8676
8682
  const name = tarName(header);
8677
8683
  if (unsafeTarPath(name)) return invalid$1("npm preset tarball contains an unsafe entry path.");
@@ -8724,7 +8730,7 @@ function validTarChecksum(header) {
8724
8730
  for (const [index, byte] of header.entries()) sum += index >= 148 && index < 156 ? 32 : byte;
8725
8731
  return sum === Number.parseInt(raw, 8);
8726
8732
  }
8727
- function isZeroBlock(block) {
8733
+ function isZeroBlock$1(block) {
8728
8734
  return block.every((byte) => byte === 0);
8729
8735
  }
8730
8736
  function invalid$1(problem) {
@@ -8817,8 +8823,8 @@ function npmDistribution(text) {
8817
8823
  } catch {
8818
8824
  return;
8819
8825
  }
8820
- const dist = isPlainRecord(value) ? value["dist"] : void 0;
8821
- if (!isPlainRecord(dist)) return;
8826
+ const dist = isPlainRecord$1(value) ? value["dist"] : void 0;
8827
+ if (!isPlainRecord$1(dist)) return;
8822
8828
  const tarball = registryTarball(dist["tarball"]);
8823
8829
  if (tarball === void 0) return;
8824
8830
  return {
@@ -8836,7 +8842,7 @@ function registryTarball(value) {
8836
8842
  return;
8837
8843
  }
8838
8844
  }
8839
- function isPlainRecord(value) {
8845
+ function isPlainRecord$1(value) {
8840
8846
  return typeof value === "object" && value !== null && !Array.isArray(value);
8841
8847
  }
8842
8848
  //#endregion
@@ -10530,7 +10536,7 @@ function positiveInteger(value) {
10530
10536
  /** What the frame says while adapters probe the machine. Same sentence the setup wizard uses. */
10531
10537
  const SCAN_PROMPT$1 = "Scanning this machine…";
10532
10538
  /** What a run with nothing to animate, or nowhere to animate it, gets instead. */
10533
- const IDLE = {
10539
+ const IDLE$1 = {
10534
10540
  close: () => {},
10535
10541
  report: () => {}
10536
10542
  };
@@ -10547,7 +10553,7 @@ const IDLE = {
10547
10553
  * existed. The frame is erased on close, so nothing it painted survives into the report.
10548
10554
  */
10549
10555
  function startScanProgress(options) {
10550
- if (options.rows.length === 0 || !isTerminal(options.stdout)) return IDLE;
10556
+ if (options.rows.length === 0 || !isTerminal(options.stdout)) return IDLE$1;
10551
10557
  const style = createStyle(options.colorDepth);
10552
10558
  const statuses = new Map(options.rows.map((row) => [row.id, "pending"]));
10553
10559
  let painted = 0;
@@ -15731,7 +15737,7 @@ function compareEntries(left, right) {
15731
15737
  if (leftRepo !== (right.repo === true)) return leftRepo ? -1 : 1;
15732
15738
  const leftConfigured = left.existing !== void 0;
15733
15739
  if (leftConfigured !== (right.existing !== void 0)) return leftConfigured ? -1 : 1;
15734
- return entryName(left).localeCompare(entryName(right));
15740
+ return entryName$1(left).localeCompare(entryName$1(right));
15735
15741
  }
15736
15742
  /** The `repo/` namespace is reserved against plugins, so the prefix alone is provenance. */
15737
15743
  function isRepoCatalogId(id) {
@@ -15739,9 +15745,9 @@ function isRepoCatalogId(id) {
15739
15745
  }
15740
15746
  /** User-facing name used by the picker and deterministic sorting. */
15741
15747
  function mcpCatalogEntryName(entry) {
15742
- return entryName(entry);
15748
+ return entryName$1(entry);
15743
15749
  }
15744
- function entryName(entry) {
15750
+ function entryName$1(entry) {
15745
15751
  return entry.catalog?.manifest.name ?? entry.existing?.name ?? entry.key;
15746
15752
  }
15747
15753
  //#endregion
@@ -15790,14 +15796,14 @@ async function resolveSourceBatch(context, sourceId, ids) {
15790
15796
  if (driver === void 0) {
15791
15797
  if (directory === void 0) return;
15792
15798
  const result = await resolveDirectorySkills(context.environment, directory, eligibleIds);
15793
- record(context, sourceId, eligibleIds, result.skills, (id) => resolutionFailure(result.diagnostics, id));
15799
+ record$1(context, sourceId, eligibleIds, result.skills, (id) => resolutionFailure(result.diagnostics, id));
15794
15800
  return;
15795
15801
  }
15796
15802
  const result = await resolveDriverSkills(context.environment, driver, eligibleIds);
15797
- record(context, sourceId, eligibleIds, result.skills, (id) => result.problems.get(id) ?? "could not be fetched from its driver");
15803
+ record$1(context, sourceId, eligibleIds, result.skills, (id) => result.problems.get(id) ?? "could not be fetched from its driver");
15798
15804
  }
15799
15805
  /** Memoizes one batch's packs, then explains every requested id the batch did not return. */
15800
- function record(context, sourceId, requestedIds, skills, failure) {
15806
+ function record$1(context, sourceId, requestedIds, skills, failure) {
15801
15807
  for (const skill of skills) context.packs.set(skillIdentity(sourceId, skill.id), skill);
15802
15808
  for (const id of requestedIds) {
15803
15809
  const identity = skillIdentity(sourceId, id);
@@ -18625,14 +18631,1722 @@ var UndoCommand = class extends Command {
18625
18631
  }
18626
18632
  };
18627
18633
  //#endregion
18628
- //#region src/run.boundary.ts
18629
- /** Runs one build-time-composed Aura distribution. */
18630
- async function runCli(distro, runtime) {
18631
- const resolved = resolveRuntime(runtime, distro.branding);
18632
- const telemetry = createTelemetryRecorder({
18633
- distroVersion: distro.branding.version,
18634
- now: resolved.now,
18635
- sink: telemetryEnabled(resolved.environmentVariables) ? distro.telemetry : void 0
18634
+ //#region src/update/limits.ts
18635
+ /**
18636
+ * Every bound the updater enforces, in one place.
18637
+ *
18638
+ * A limit that lives next to its single use gets relaxed by whoever is debugging that use. These
18639
+ * are the numbers a reviewer needs to see together to know an update cannot exhaust the disk, the
18640
+ * heap, the network, or the user's patience.
18641
+ */
18642
+ /** Bytes accepted for one release archive, streamed and counted rather than buffered. */
18643
+ const MAX_ARCHIVE_BYTES = 268435456;
18644
+ /**
18645
+ * Bytes accepted from an archive's extracted entries, so a compression bomb cannot fill a disk.
18646
+ *
18647
+ * Larger than the archive bound because the archive is compressed, and small enough that the worst
18648
+ * case is a temporary file the transaction removes rather than a full disk.
18649
+ */
18650
+ const MAX_EXTRACTED_BYTES = 536870912;
18651
+ /** Bytes accepted for one release-metadata document. */
18652
+ const MAX_METADATA_BYTES = 524288;
18653
+ /** Bytes accepted for one cached metadata entry. */
18654
+ const MAX_CACHE_BYTES = 65536;
18655
+ /**
18656
+ * How far ahead of now a signed manifest's `expiresAt` may sit.
18657
+ *
18658
+ * A signature proves who wrote a manifest, never that it is the newest one they wrote. Without a
18659
+ * bound, anything that can serve a stale-but-valid copy — a compromised edge, a caching proxy, a
18660
+ * mirror left behind — pins a fleet to a release forever, and the updater reports nothing because
18661
+ * "no newer release" is the quiet path. The window is what turns that freeze into an expiry.
18662
+ *
18663
+ * Capped rather than merely required, because a publisher who dates a manifest a decade out has
18664
+ * satisfied the field and rebuilt the same problem.
18665
+ */
18666
+ const MAX_MANIFEST_FRESHNESS_MS = 2592e6;
18667
+ /**
18668
+ * Milliseconds the whole startup update may spend before the user's command starts.
18669
+ *
18670
+ * The one number that bounds the wait, because it is the only one the user experiences. Every step
18671
+ * below has its own ceiling, but a per-step bound is not an aggregate: metadata, a probe of the
18672
+ * installed binary, the transfer, and a probe of the staged one each finishing just inside their
18673
+ * own limit is a command that has not started yet. The download is given whatever is left of this,
18674
+ * so a slow transfer is abandoned rather than allowed to consume the sum of the other steps too.
18675
+ */
18676
+ const STARTUP_UPDATE_BUDGET_MS = 24e4;
18677
+ /** Milliseconds one metadata request may take. */
18678
+ const METADATA_TIMEOUT_MS = 1e4;
18679
+ /** Milliseconds a version probe of an executable may take. */
18680
+ const VERSION_PROBE_TIMEOUT_MS = 3e4;
18681
+ /** How long a successful "already current" check stays fresh. */
18682
+ const CHECK_FRESH_MS = 864e5;
18683
+ /** How long a failed check waits before the next one, silently. */
18684
+ const CHECK_RETRY_MS = 36e5;
18685
+ /** First backoff step after an installation failure, doubled per attempt. */
18686
+ const INSTALL_BACKOFF_BASE_MS = 9e5;
18687
+ /** Ceiling the installation backoff doubles up to. */
18688
+ const INSTALL_BACKOFF_MAX_MS = 864e5;
18689
+ //#endregion
18690
+ //#region src/update/download.boundary.ts
18691
+ const REDIRECT_STATUSES = /* @__PURE__ */ new Set([
18692
+ 301,
18693
+ 302,
18694
+ 303,
18695
+ 307,
18696
+ 308
18697
+ ]);
18698
+ /** Progress reports emitted across one download, spread evenly over its declared length. */
18699
+ const PROGRESS_STEPS = 100;
18700
+ /**
18701
+ * Creates the archive downloader: a bounded, TLS-only stream straight to disk.
18702
+ *
18703
+ * This is the one place in the updater that reaches the network directly. `Environment.httpGet`
18704
+ * cannot do this job — it buffers into memory and refuses redirects outright — and a release
18705
+ * archive is both too large to hold in the heap and, for a private repository, served through a
18706
+ * redirect to a temporary signed URL. The rules `httpGet` enforces are reimplemented rather than
18707
+ * relaxed: HTTPS only, a hop limit, and `Authorization` stripped the moment the origin changes.
18708
+ */
18709
+ function createUpdateDownload() {
18710
+ return async (request) => {
18711
+ try {
18712
+ return await stream(request);
18713
+ } catch {
18714
+ return {
18715
+ kind: "failure",
18716
+ reason: "network"
18717
+ };
18718
+ }
18719
+ };
18720
+ }
18721
+ async function stream(request) {
18722
+ const response = await follow(request, AbortSignal.timeout(request.timeoutMs));
18723
+ if (typeof response === "string") return {
18724
+ kind: "failure",
18725
+ reason: response
18726
+ };
18727
+ if (response.status !== 200 || response.body === null) return {
18728
+ kind: "failure",
18729
+ reason: "network"
18730
+ };
18731
+ const declared = response.headers.get("content-length");
18732
+ if (announcesReleaseLength(response) && declared !== String(request.expectedBytes)) return {
18733
+ kind: "failure",
18734
+ reason: "unexpected-length"
18735
+ };
18736
+ return await writeBody(response.body, request);
18737
+ }
18738
+ /**
18739
+ * Whether `content-length` describes the bytes this download will actually see.
18740
+ *
18741
+ * A proxy that applies its own `Content-Encoding` leaves the header describing the compressed form
18742
+ * while `fetch` hands over the decoded one, so comparing the two rejects a download that is
18743
+ * perfectly good. The count and the digest are both re-checked once the body has been read, so
18744
+ * skipping the header here costs nothing.
18745
+ */
18746
+ function announcesReleaseLength(response) {
18747
+ const encoding = response.headers.get("content-encoding")?.trim().toLowerCase();
18748
+ return response.headers.get("content-length") !== null && (encoding === void 0 || encoding === "" || encoding === "identity");
18749
+ }
18750
+ /** Streams the body to a fresh file, counting and hashing as it goes. */
18751
+ async function writeBody(body, request) {
18752
+ const hash = createHash("sha256");
18753
+ let received = 0;
18754
+ const step = Math.ceil(request.expectedBytes / PROGRESS_STEPS);
18755
+ let reported = 0;
18756
+ const handle = await open(request.destinationPath, "wx", 384);
18757
+ try {
18758
+ for await (const chunk of body) {
18759
+ received += chunk.byteLength;
18760
+ if (received > request.expectedBytes) return {
18761
+ kind: "failure",
18762
+ reason: "too-large"
18763
+ };
18764
+ hash.update(chunk);
18765
+ await handle.write(chunk);
18766
+ if (received - reported >= step) {
18767
+ reported = received;
18768
+ request.onProgress?.(received, request.expectedBytes);
18769
+ }
18770
+ }
18771
+ if (received !== request.expectedBytes) return {
18772
+ kind: "failure",
18773
+ reason: "unexpected-length"
18774
+ };
18775
+ await handle.sync();
18776
+ return {
18777
+ kind: "downloaded",
18778
+ sha256: hash.digest("hex")
18779
+ };
18780
+ } finally {
18781
+ await handle.close();
18782
+ }
18783
+ }
18784
+ /**
18785
+ * Follows redirects by hand so each hop is vetted before it is taken.
18786
+ *
18787
+ * `Authorization` survives only a same-origin hop. GitHub answers an authenticated asset request
18788
+ * with a redirect to a signed storage URL that needs no credential of its own, so forwarding one
18789
+ * there would hand a repository token to a host the caller never named.
18790
+ */
18791
+ async function follow(request, signal) {
18792
+ let url = vetHttpUrl(request.url);
18793
+ let headers = { ...request.headers };
18794
+ for (let hop = 0; hop <= 5; hop += 1) {
18795
+ if (!(url instanceof URL)) return url === "insecure-url" ? "insecure-url" : "network";
18796
+ const response = await fetch(url, {
18797
+ headers,
18798
+ method: "GET",
18799
+ redirect: "manual",
18800
+ signal
18801
+ });
18802
+ const location = response.headers.get("location");
18803
+ if (!REDIRECT_STATUSES.has(response.status) || location === null) return response;
18804
+ const origin = url.origin;
18805
+ url = vetHttpUrl(new URL(location, url).href);
18806
+ if (url instanceof URL && url.origin !== origin) headers = withoutAuthorization(headers);
18807
+ }
18808
+ return "network";
18809
+ }
18810
+ function withoutAuthorization(headers) {
18811
+ return Object.fromEntries(Object.entries(headers).filter(([name]) => name.toLowerCase() !== "authorization"));
18812
+ }
18813
+ //#endregion
18814
+ //#region src/update/host.boundary.ts
18815
+ /** A version line and nothing else: one canonical-shaped version on one line. */
18816
+ const VERSION_LINE = /^[0-9][0-9A-Za-z.+-]*$/u;
18817
+ /**
18818
+ * The process seam the updater runs against.
18819
+ *
18820
+ * Named as a boundary because it is one: the process id, the ability to signal another process,
18821
+ * and forking a child are ambient state everywhere else in the CLI is forbidden from reading.
18822
+ * Everything below this file takes {@link UpdateHost} as a parameter, so the exception is one
18823
+ * module wide.
18824
+ */
18825
+ const UPDATE_HOST = {
18826
+ download: createUpdateDownload(),
18827
+ isProcessAlive,
18828
+ pid: process$1.pid,
18829
+ probeVersion
18830
+ };
18831
+ /**
18832
+ * Whether a process id is still running, from the perspective of this user.
18833
+ *
18834
+ * Signal `0` performs the permission and existence checks without delivering anything. `EPERM`
18835
+ * means the process exists and belongs to someone else, which still counts as alive: the lock it
18836
+ * holds is not this run's to break.
18837
+ */
18838
+ function isProcessAlive(pid) {
18839
+ if (!Number.isInteger(pid) || pid <= 0) return false;
18840
+ try {
18841
+ process$1.kill(pid, 0);
18842
+ return true;
18843
+ } catch (error) {
18844
+ return error instanceof Error && "code" in error && error.code === "EPERM";
18845
+ }
18846
+ }
18847
+ /**
18848
+ * Asks an executable what version it is.
18849
+ *
18850
+ * The child's environment is exactly what the caller passed, which is how a staged binary is
18851
+ * verified without giving it the chance to start an update of its own. Any outcome that is not one
18852
+ * well-formed version line reads as unknown, and an unknown version never satisfies the equality
18853
+ * the installer requires before it replaces anything.
18854
+ */
18855
+ function probeVersion(executablePath, environmentVariables) {
18856
+ return new Promise((resolve) => {
18857
+ execFile(executablePath, ["--version"], {
18858
+ encoding: "utf8",
18859
+ env: { ...environmentVariables },
18860
+ timeout: VERSION_PROBE_TIMEOUT_MS
18861
+ }, (error, stdout) => {
18862
+ const version = stdout.trim();
18863
+ resolve(error === null && VERSION_LINE.test(version) ? version : void 0);
18864
+ });
18865
+ });
18866
+ }
18867
+ //#endregion
18868
+ //#region src/update/narrow.ts
18869
+ /**
18870
+ * Narrowing helpers for provider responses.
18871
+ *
18872
+ * Every release document is unknown input from a server, so nothing reads a field without proving
18873
+ * its shape first. These return `undefined` rather than throwing: a provider turns absence into a
18874
+ * refusal, and a refusal is never fatal to the command the user actually asked for.
18875
+ */
18876
+ /** 64 lowercase hexadecimal characters, the only digest form the updater accepts. */
18877
+ const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
18878
+ const GITHUB_DIGEST_PREFIX = "sha256:";
18879
+ function asRecord(value) {
18880
+ return isPlainRecord(value) ? value : void 0;
18881
+ }
18882
+ function asArray(value) {
18883
+ return Array.isArray(value) ? value : void 0;
18884
+ }
18885
+ /** A non-empty string, since an empty name or URL is never a value the updater can act on. */
18886
+ function asText(value) {
18887
+ return typeof value === "string" && value !== "" ? value : void 0;
18888
+ }
18889
+ /** A boolean, and only a boolean: a missing `immutable` field must not read as `false`. */
18890
+ function asFlag(value) {
18891
+ return typeof value === "boolean" ? value : void 0;
18892
+ }
18893
+ /** A positive safe integer within `ceiling`, which is what a byte size has to be to be usable. */
18894
+ function asSize(value, ceiling) {
18895
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) return;
18896
+ return value <= ceiling ? value : void 0;
18897
+ }
18898
+ /** A bare lowercase SHA-256 digest. */
18899
+ function asDigest(value) {
18900
+ const text = asText(value);
18901
+ return text !== void 0 && SHA256_PATTERN.test(text) ? text : void 0;
18902
+ }
18903
+ /** GitHub's `sha256:<hex>` asset digest, reduced to the bare hexadecimal form. */
18904
+ function asGitHubDigest(value) {
18905
+ const text = asText(value);
18906
+ if (text === void 0 || !text.startsWith(GITHUB_DIGEST_PREFIX)) return;
18907
+ return asDigest(text.slice(7));
18908
+ }
18909
+ /** Parses a document, treating any malformed body as an absent one. */
18910
+ function parseJson(text) {
18911
+ try {
18912
+ return JSON.parse(text);
18913
+ } catch {
18914
+ return;
18915
+ }
18916
+ }
18917
+ function isPlainRecord(value) {
18918
+ return typeof value === "object" && value !== null && !Array.isArray(value);
18919
+ }
18920
+ //#endregion
18921
+ //#region src/update/cache.ts
18922
+ /**
18923
+ * Where update metadata lives, beside every other cache Aura keeps.
18924
+ *
18925
+ * The entry is machine state, not workspace content: `0700` directories, `0600` files, and
18926
+ * temporary-file renames, all inherited from the shared cache primitives.
18927
+ */
18928
+ const NAMESPACE = "distribution-updates";
18929
+ const OUTCOMES = {
18930
+ "check-failed": "check-failed",
18931
+ current: "current",
18932
+ "install-failed": "install-failed"
18933
+ };
18934
+ /**
18935
+ * Reads the entry for one source, or `undefined` for every reason it cannot be used.
18936
+ *
18937
+ * Corruption, truncation, and a timestamp from the future are all misses rather than errors. A
18938
+ * changed source identity resolves to a different hashed path. A cache is only an optimization.
18939
+ */
18940
+ async function readUpdateCache(homeDir, identity, now) {
18941
+ const value = await readCacheEnvelope(cacheLocation({ homeDir }, NAMESPACE, identity), MAX_CACHE_BYTES);
18942
+ if (value === void 0) return;
18943
+ const checkedAt = value["checkedAt"];
18944
+ const outcome = value["outcome"];
18945
+ if (typeof checkedAt !== "number" || checkedAt > now || typeof outcome !== "string") return;
18946
+ return narrowEntry(value, checkedAt, outcome);
18947
+ }
18948
+ /** Stores one entry, treating any write failure as nothing having happened. */
18949
+ async function writeUpdateCache(homeDir, identity, entry) {
18950
+ await writeCacheEnvelope(cacheLocation({ homeDir }, NAMESPACE, identity), { ...entry });
18951
+ }
18952
+ /**
18953
+ * Whether this run should ask the source for release metadata.
18954
+ *
18955
+ * The cadence is what keeps startup cheap and quiet: one successful check a day, one silent retry
18956
+ * an hour after a failure, and an exponential backoff per candidate version after an installation
18957
+ * that did not complete — so a machine that cannot write to its own install directory asks once,
18958
+ * then twice a day, rather than on every command.
18959
+ */
18960
+ function shouldCheck(entry, now) {
18961
+ if (entry === void 0) return true;
18962
+ if (entry.outcome === "current") return now - entry.checkedAt >= CHECK_FRESH_MS;
18963
+ if (entry.outcome === "check-failed") return now - entry.checkedAt >= CHECK_RETRY_MS;
18964
+ return now >= nextInstallAttempt(entry);
18965
+ }
18966
+ /**
18967
+ * Attempts already spent on one candidate version.
18968
+ *
18969
+ * A different version starts at zero: backoff exists to stop retrying a release that will not
18970
+ * install, not to punish the next one for it.
18971
+ */
18972
+ function attemptsFor(entry, version) {
18973
+ return entry?.failedVersion === version ? entry.failedAttempts ?? 0 : 0;
18974
+ }
18975
+ function nextInstallAttempt(entry) {
18976
+ const attempts = entry.failedAttempts ?? 0;
18977
+ if (attempts === 0) return entry.checkedAt;
18978
+ const delay = Math.min(INSTALL_BACKOFF_BASE_MS * 2 ** (attempts - 1), INSTALL_BACKOFF_MAX_MS);
18979
+ return entry.checkedAt + delay;
18980
+ }
18981
+ function narrowEntry(value, checkedAt, rawOutcome) {
18982
+ const outcome = OUTCOMES[rawOutcome];
18983
+ const failedVersion = asText(value["failedVersion"]);
18984
+ if (outcome === void 0 || outcome === "install-failed" && failedVersion === void 0) return;
18985
+ return {
18986
+ checkedAt,
18987
+ ...narrowEtag(value),
18988
+ ...narrowFailure(value),
18989
+ outcome
18990
+ };
18991
+ }
18992
+ function narrowEtag(value) {
18993
+ const etag = asText(value["etag"]);
18994
+ return etag === void 0 ? {} : { etag };
18995
+ }
18996
+ function narrowFailure(value) {
18997
+ const failedAttempts = asSize(value["failedAttempts"], Number.MAX_SAFE_INTEGER);
18998
+ const failedVersion = asText(value["failedVersion"]);
18999
+ return {
19000
+ ...failedAttempts === void 0 ? {} : { failedAttempts },
19001
+ ...failedVersion === void 0 ? {} : { failedVersion }
19002
+ };
19003
+ }
19004
+ //#endregion
19005
+ //#region src/update/diagnostics.ts
19006
+ /**
19007
+ * The updater's two diagnostic surfaces: a trace for whoever wired the distribution up, and a
19008
+ * progress line for whoever is waiting on the download.
19009
+ *
19010
+ * Neither belongs to the message contract in `docs/cli-ux.md`. The trace stays off unless a
19011
+ * variable asks for it, and the progress line is painted only on a terminal that can erase it
19012
+ * again — so redirected output, piped output, and `--json` stay byte-identical either way.
19013
+ */
19014
+ /** Suffix on a distribution's disable variable, which is what names its debug variable. */
19015
+ const DEBUG_SUFFIX = "_DEBUG";
19016
+ const ENABLED_VALUES = /* @__PURE__ */ new Set([
19017
+ "1",
19018
+ "on",
19019
+ "true",
19020
+ "yes"
19021
+ ]);
19022
+ /** What the progress frame says while the archive streams. */
19023
+ const DOWNLOAD_PROMPT = "Downloading…";
19024
+ /** The distribution-specific variable that disables startup updates. */
19025
+ function updateEnvironmentVariable(command) {
19026
+ return `${command.replace(/[^A-Za-z0-9]+/gu, "_").replace(/^_+|_+$/gu, "").toUpperCase()}_UPDATE`;
19027
+ }
19028
+ /**
19029
+ * The debug writer for one distribution.
19030
+ *
19031
+ * Derived from the command name rather than configured: `acme-dev` gets `ACME_DEV_UPDATE` and its
19032
+ * trace gets `ACME_DEV_UPDATE_DEBUG`, leaving neither as a field a distribution can mistype.
19033
+ *
19034
+ * It exists because every refusal in this subsystem is deliberately silent. That is right for the
19035
+ * user, whose command is the thing they asked about — and useless for the author of a distribution
19036
+ * whose updates are simply not happening, with nine gates and no way to tell which one fired.
19037
+ */
19038
+ function createUpdateDebug(disableEnvironmentVariable, environmentVariables, stderr) {
19039
+ const value = environmentVariables[`${disableEnvironmentVariable}${DEBUG_SUFFIX}`];
19040
+ if (value === void 0 || !ENABLED_VALUES.has(value.trim().toLowerCase())) return () => void 0;
19041
+ return (message) => {
19042
+ stderr.write(`update: ${message}\n`);
19043
+ };
19044
+ }
19045
+ /** What a run with nowhere to paint gets instead. */
19046
+ const IDLE = {
19047
+ close: () => {},
19048
+ report: void 0
19049
+ };
19050
+ /**
19051
+ * Paints how much of the release archive has arrived.
19052
+ *
19053
+ * The download is the one part of an update the user waits on, and the archive is tens of
19054
+ * megabytes: on a thin connection a single unchanging line is indistinguishable from a hung
19055
+ * command. Erased on close, so nothing it painted survives into the outcome line.
19056
+ */
19057
+ function startUpdateProgress(stderr) {
19058
+ if (!isTerminal(stderr)) return IDLE;
19059
+ let painted = 0;
19060
+ let closed = false;
19061
+ return {
19062
+ close: () => {
19063
+ if (closed) return;
19064
+ closed = true;
19065
+ stderr.write(eraseFrame(painted));
19066
+ painted = 0;
19067
+ },
19068
+ report: (received, total) => {
19069
+ if (closed) return;
19070
+ const frame = ` ${DOWNLOAD_PROMPT} ${String(percentage(received, total))}%\n`;
19071
+ stderr.write(`${eraseFrame(painted)}${frame}`);
19072
+ painted = countFrameRows(frame, terminalDimension(stderr, "columns") ?? 80);
19073
+ }
19074
+ };
19075
+ }
19076
+ /** Whole percent, never reaching 100 before the transfer has actually finished. */
19077
+ function percentage(received, total) {
19078
+ if (total <= 0) return 99;
19079
+ return Math.min(99, Math.floor(received / total * 100));
19080
+ }
19081
+ //#endregion
19082
+ //#region src/update/target.ts
19083
+ /**
19084
+ * The version a source build reports.
19085
+ *
19086
+ * Only release CI stamps a real version into the distribution manifest, so a developer running a
19087
+ * checkout is structurally indistinguishable from the newest release. Refusing this value keeps a
19088
+ * source build from replacing itself with a published binary.
19089
+ */
19090
+ const UNSTAMPED_VERSION = "0.0.0";
19091
+ const TARGETS = {
19092
+ "darwin-arm64": "darwin-arm64",
19093
+ "darwin-x64": "darwin-x64",
19094
+ "linux-arm64": "linux-arm64",
19095
+ "linux-x64": "linux-x64"
19096
+ };
19097
+ /** The release target for one installation, or `undefined` when no release names this machine. */
19098
+ function releaseTarget(current) {
19099
+ return TARGETS[`${current.platform}-${current.arch}`];
19100
+ }
19101
+ /**
19102
+ * Whether a version is canonical semver a release can be selected by.
19103
+ *
19104
+ * Canonical rather than merely parseable: `1.4` and `v1.4.0` both describe a release, but only one
19105
+ * spelling can be compared against a tag, a probed `--version`, and a cached candidate and agree
19106
+ * with itself every time.
19107
+ */
19108
+ function isInstallableVersion(value) {
19109
+ return valid(value) === value && value !== UNSTAMPED_VERSION;
19110
+ }
19111
+ /** Whether `candidate` is a strictly newer release than `current`. Prereleases order as semver. */
19112
+ function isNewerVersion(candidate, current) {
19113
+ return valid(candidate) === candidate && valid(current) === current && gt(candidate, current);
19114
+ }
19115
+ //#endregion
19116
+ //#region src/update/eligibility.ts
19117
+ /** Values that turn startup updates off, matching how `AURA_TELEMETRY=off` reads today. */
19118
+ const DISABLED_VALUES = /* @__PURE__ */ new Set([
19119
+ "0",
19120
+ "false",
19121
+ "no",
19122
+ "off"
19123
+ ]);
19124
+ /**
19125
+ * Whether this run may install over its own executable.
19126
+ *
19127
+ * Every clause is a refusal, and the whole gate resolves before a single byte is requested: a run
19128
+ * that is not eligible makes no network request and touches no file. The interactive and `CI`
19129
+ * clauses are what keep a pipeline pinned to the binary it selected — a script that pinned
19130
+ * `v0.5.0` must still be running `0.5.0` an hour later.
19131
+ */
19132
+ async function eligibleInstallation(request) {
19133
+ const { current, version } = request;
19134
+ if (version === void 0 || !isInstallableVersion(version)) return {
19135
+ kind: "refused",
19136
+ reason: "unstamped-version"
19137
+ };
19138
+ const target = releaseTarget(current);
19139
+ if (target === void 0) return {
19140
+ kind: "refused",
19141
+ reason: "unsupported-target"
19142
+ };
19143
+ const blocked = blockedBy(request);
19144
+ if (blocked !== void 0) return {
19145
+ kind: "refused",
19146
+ reason: blocked
19147
+ };
19148
+ if (!await isReplaceableFile(current.execPath)) return {
19149
+ kind: "refused",
19150
+ reason: "not-a-regular-file"
19151
+ };
19152
+ return {
19153
+ installation: {
19154
+ executablePath: current.execPath,
19155
+ target,
19156
+ version
19157
+ },
19158
+ kind: "eligible"
19159
+ };
19160
+ }
19161
+ /** The environment and terminal half of the gate, separated so its clauses stay readable. */
19162
+ function blockedBy(request) {
19163
+ if (isInformationalRun(request.argv)) return "informational-run";
19164
+ if (turnedOff(request)) return "disabled";
19165
+ if (inContinuousIntegration(request)) return "continuous-integration";
19166
+ return ownsTerminal(request) ? void 0 : "not-a-terminal";
19167
+ }
19168
+ /** Help and version output must remain immediate, including the argument-free root help screen. */
19169
+ function isInformationalRun(argv) {
19170
+ return argv.length === 0 || argv.some((value) => value === "--help" || value === "-h" || value === "--version");
19171
+ }
19172
+ function turnedOff(request) {
19173
+ const value = request.environmentVariables[request.disableEnvironmentVariable];
19174
+ return value !== void 0 && DISABLED_VALUES.has(value.trim().toLowerCase());
19175
+ }
19176
+ /**
19177
+ * Whether this looks like an automated runner.
19178
+ *
19179
+ * Any `CI` value counts. Providers disagree on whether it is `true`, `1`, or their own name, and an
19180
+ * update nobody asked for is the wrong way to discover which one this runner uses.
19181
+ */
19182
+ function inContinuousIntegration(request) {
19183
+ const value = request.environmentVariables["CI"];
19184
+ return value !== void 0 && value !== "" && value !== "0";
19185
+ }
19186
+ /** All three streams, so neither a redirected report nor a piped-in script gets an update. */
19187
+ function ownsTerminal(request) {
19188
+ return isTerminal(request.stdin) && isTerminal(request.stdout) && isTerminal(request.stderr);
19189
+ }
19190
+ /**
19191
+ * Whether the path is a regular file the installer can rename over.
19192
+ *
19193
+ * `lstat` rather than `stat`: a symlink means some other installation — a package manager's shim,
19194
+ * a version manager's current-release pointer — owns this name, and replacing the link would
19195
+ * either detach it from its manager or write through it into a directory Aura does not own.
19196
+ */
19197
+ async function isReplaceableFile(path) {
19198
+ try {
19199
+ return (await lstat(path)).isFile();
19200
+ } catch {
19201
+ return false;
19202
+ }
19203
+ }
19204
+ //#endregion
19205
+ //#region src/update/lock.ts
19206
+ /**
19207
+ * Takes the per-executable update lock, or gives up.
19208
+ *
19209
+ * Exclusive creation is the whole mechanism: two updaters racing on the same executable both call
19210
+ * `open(…, "wx")` and exactly one succeeds. Losing is not an error — the other process is already
19211
+ * installing the same release, and the command the user asked for runs either way.
19212
+ */
19213
+ async function acquireUpdateLock(request) {
19214
+ const attempt = await claim(request);
19215
+ if (attempt.kind !== "held") return attempt;
19216
+ return await reclaimStale(request) ? claim(request) : attempt;
19217
+ }
19218
+ async function claim(request) {
19219
+ const token = randomUUID();
19220
+ try {
19221
+ const handle = await open(request.lockPath, "wx", 384);
19222
+ try {
19223
+ await handle.write(JSON.stringify({
19224
+ pid: request.host.pid,
19225
+ startedAt: request.now,
19226
+ token
19227
+ }));
19228
+ } finally {
19229
+ await handle.close();
19230
+ }
19231
+ return {
19232
+ kind: "acquired",
19233
+ lock: { release: () => removeOwned(request.lockPath, token) }
19234
+ };
19235
+ } catch (error) {
19236
+ return { kind: error instanceof Error && "code" in error && error.code === "EEXIST" ? "held" : "unavailable" };
19237
+ }
19238
+ }
19239
+ /**
19240
+ * Removes a lock whose owner is gone.
19241
+ *
19242
+ * Both conditions are required. Age alone would break a slow but healthy download on a thin
19243
+ * connection; a missing process alone would race a holder that has not written its record yet.
19244
+ * An unreadable or malformed lock counts as expired once it is old enough — nothing that can be
19245
+ * asked about it will ever answer.
19246
+ */
19247
+ async function reclaimStale(request) {
19248
+ const contents = await read(request.lockPath);
19249
+ const record = asRecord(parseJson(contents));
19250
+ const startedAt = asSize(record?.["startedAt"], Number.MAX_SAFE_INTEGER) ?? await modifiedAt(request.lockPath);
19251
+ if (startedAt === void 0 || request.now - startedAt < 6e5) return false;
19252
+ const pid = asSize(record?.["pid"], Number.MAX_SAFE_INTEGER);
19253
+ if (pid !== void 0 && request.host.isProcessAlive(pid)) return false;
19254
+ return await removeUnchanged(request.lockPath, contents);
19255
+ }
19256
+ /**
19257
+ * When the lock file was last written, used when its contents cannot say.
19258
+ *
19259
+ * An updater killed between creating the lock and writing its record leaves a file that names no
19260
+ * process. Without this fallback that file would block every future update on the machine.
19261
+ */
19262
+ async function modifiedAt(path) {
19263
+ try {
19264
+ return (await stat(path)).mtimeMs;
19265
+ } catch {
19266
+ return;
19267
+ }
19268
+ }
19269
+ async function read(path) {
19270
+ try {
19271
+ return await readFile(path, "utf8");
19272
+ } catch {
19273
+ return "";
19274
+ }
19275
+ }
19276
+ async function remove(path) {
19277
+ try {
19278
+ await unlink(path);
19279
+ } catch {}
19280
+ }
19281
+ /** Removes a stale lock only while it is still the exact record that was inspected. */
19282
+ async function removeUnchanged(path, expected) {
19283
+ const reclaimPath = `${path}.reclaim-${createHash("sha256").update(expected, "utf8").digest("hex").slice(0, 16)}`;
19284
+ try {
19285
+ await link(path, reclaimPath);
19286
+ } catch {
19287
+ return false;
19288
+ }
19289
+ try {
19290
+ if (await read(reclaimPath) !== expected || await read(path) !== expected) return false;
19291
+ await unlink(path);
19292
+ return true;
19293
+ } catch {
19294
+ return false;
19295
+ } finally {
19296
+ await remove(reclaimPath);
19297
+ }
19298
+ }
19299
+ /** A holder must never unlink a successor that reused the same path. */
19300
+ async function removeOwned(path, token) {
19301
+ if (asRecord(parseJson(await read(path)))?.["token"] === token) await remove(path);
19302
+ }
19303
+ //#endregion
19304
+ //#region src/update/tar-header.ts
19305
+ const NAME = {
19306
+ length: 100,
19307
+ offset: 0
19308
+ };
19309
+ const SIZE = {
19310
+ length: 12,
19311
+ offset: 124
19312
+ };
19313
+ const CHECKSUM = {
19314
+ length: 8,
19315
+ offset: 148
19316
+ };
19317
+ const TYPEFLAG_OFFSET = 156;
19318
+ const PREFIX = {
19319
+ length: 155,
19320
+ offset: 345
19321
+ };
19322
+ /** Type flags for a plain file. `0` is ustar; NUL is the historic spelling of the same thing. */
19323
+ const REGULAR_TYPES = /* @__PURE__ */ new Set(["0", "\0"]);
19324
+ /** Whether a block is all zeroes, which is how a tar announces its own end. */
19325
+ function isZeroBlock(block) {
19326
+ return block.every((byte) => byte === 0);
19327
+ }
19328
+ /**
19329
+ * One header, or `undefined` for any block this extractor refuses to act on.
19330
+ *
19331
+ * Refusal is deliberately undifferentiated: the caller aborts the whole archive either way, and a
19332
+ * reason string would only invite someone to relax a specific case later.
19333
+ */
19334
+ function parseTarHeader(block) {
19335
+ if (!hasValidChecksum(block)) return;
19336
+ if (!REGULAR_TYPES.has(String.fromCharCode(block[TYPEFLAG_OFFSET] ?? 0))) return;
19337
+ const size = parseOctal(block, SIZE);
19338
+ const name = entryName(block);
19339
+ return size === void 0 || name === void 0 ? void 0 : {
19340
+ name,
19341
+ size
19342
+ };
19343
+ }
19344
+ /**
19345
+ * The entry path, refused unless it is a plain relative name.
19346
+ *
19347
+ * Absolute paths, parent traversal, and Windows separators are all ways to make an extraction land
19348
+ * somewhere the caller did not choose, and none of them appear in an archive this project builds.
19349
+ */
19350
+ function entryName(block) {
19351
+ const prefix = readString(block, PREFIX);
19352
+ const base = readString(block, NAME);
19353
+ if (base === "") return;
19354
+ const joined = prefix === "" ? base : `${prefix}/${base}`;
19355
+ const normalized = joined.startsWith("./") ? joined.slice(2) : joined;
19356
+ const segments = normalized.split("/");
19357
+ if (normalized.startsWith("/") || normalized.includes("\\")) return;
19358
+ return segments.some((segment) => segment === ".." || segment === "") ? void 0 : normalized;
19359
+ }
19360
+ /**
19361
+ * The header checksum, which every tar writer sets and every reader is expected to verify.
19362
+ *
19363
+ * Checked here because it is the cheapest way to notice that the stream has drifted out of block
19364
+ * alignment — a state in which arbitrary payload bytes would otherwise be read as a header.
19365
+ */
19366
+ function hasValidChecksum(block) {
19367
+ const declared = parseOctal(block, CHECKSUM);
19368
+ if (declared === void 0) return false;
19369
+ let sum = 0;
19370
+ for (const [index, byte] of block.entries()) {
19371
+ const inChecksum = index >= CHECKSUM.offset && index < CHECKSUM.offset + CHECKSUM.length;
19372
+ sum += inChecksum ? 32 : byte;
19373
+ }
19374
+ return sum === declared;
19375
+ }
19376
+ /**
19377
+ * A NUL- or space-terminated octal field.
19378
+ *
19379
+ * Base-256 encoded fields — the GNU extension for sizes beyond 8 GiB — are refused rather than
19380
+ * decoded: nothing this extracts is that large, and the size field is what bounds the write.
19381
+ */
19382
+ function parseOctal(block, field) {
19383
+ if ((block.subarray(field.offset, field.offset + field.length)[0] ?? 0) & 128) return;
19384
+ const text = readString(block, field).trim();
19385
+ if (text === "") return 0;
19386
+ if (!/^[0-7]+$/u.test(text)) return;
19387
+ const value = Number.parseInt(text, 8);
19388
+ return Number.isSafeInteger(value) && value >= 0 ? value : void 0;
19389
+ }
19390
+ function readString(block, field) {
19391
+ const bytes = block.subarray(field.offset, field.offset + field.length);
19392
+ const end = bytes.indexOf(0);
19393
+ return new TextDecoder().decode(end === -1 ? bytes : bytes.subarray(0, end)).replace(/\0+$/u, "");
19394
+ }
19395
+ //#endregion
19396
+ //#region src/update/archive.ts
19397
+ /**
19398
+ * Extracts the expected files from a gzip tar, and refuses everything else.
19399
+ *
19400
+ * Allow-listed by name rather than filtered after the fact: the extractor never creates a path the
19401
+ * caller did not name, so no header field — however hostile — can decide where a byte lands.
19402
+ */
19403
+ async function extractArchive(request) {
19404
+ const extractor = new TarExtractor(request);
19405
+ const source = createReadStream(request.archivePath);
19406
+ try {
19407
+ for await (const chunk of source.pipe(createGunzip())) {
19408
+ if (!(chunk instanceof Uint8Array)) return "unreadable-archive";
19409
+ extractor.push(chunk);
19410
+ await extractor.drain();
19411
+ if (extractor.failure !== void 0) return extractor.failure;
19412
+ if (extractor.done) break;
19413
+ }
19414
+ } catch {
19415
+ return "unreadable-archive";
19416
+ } finally {
19417
+ source.destroy();
19418
+ await extractor.close();
19419
+ }
19420
+ return extractor.failure ?? (extractor.extracted(request.requiredEntry) ? void 0 : "missing-executable");
19421
+ }
19422
+ /** A tar reader that consumes a byte stream one 512-byte block at a time. */
19423
+ var TarExtractor = class {
19424
+ failure;
19425
+ #request;
19426
+ #seen = /* @__PURE__ */ new Set();
19427
+ #chunks = [];
19428
+ #queued = 0;
19429
+ #state = "header";
19430
+ #remaining = 0;
19431
+ #padding = 0;
19432
+ #pending;
19433
+ #sink;
19434
+ #written = 0;
19435
+ #zeroBlocks = 0;
19436
+ #done = false;
19437
+ constructor(request) {
19438
+ this.#request = request;
19439
+ }
19440
+ push(chunk) {
19441
+ this.#chunks.push(chunk);
19442
+ this.#queued += chunk.byteLength;
19443
+ }
19444
+ extracted(name) {
19445
+ return this.#seen.has(name);
19446
+ }
19447
+ get done() {
19448
+ return this.#done;
19449
+ }
19450
+ /** Consumes as much of the queue as the current state allows, then waits for more bytes. */
19451
+ async drain() {
19452
+ while (this.failure === void 0 && !this.#done) if (!(this.#state === "header" ? await this.#readHeader() : this.#state === "body" ? await this.#readBody() : await this.#endEntry())) return;
19453
+ }
19454
+ async close() {
19455
+ const sink = this.#sink;
19456
+ this.#sink = void 0;
19457
+ this.#pending = void 0;
19458
+ this.#chunks = [];
19459
+ this.#queued = 0;
19460
+ try {
19461
+ await sink?.close();
19462
+ } catch {}
19463
+ }
19464
+ async #readHeader() {
19465
+ const block = this.#take(512);
19466
+ if (block === void 0) return false;
19467
+ if (isZeroBlock(block)) {
19468
+ this.#zeroBlocks += 1;
19469
+ this.#done = this.#zeroBlocks === 2;
19470
+ return true;
19471
+ }
19472
+ this.#zeroBlocks = 0;
19473
+ const entry = parseTarHeader(block);
19474
+ if (entry === void 0) {
19475
+ this.failure = "unexpected-entry";
19476
+ return false;
19477
+ }
19478
+ return await this.#openEntry(entry.name, entry.size);
19479
+ }
19480
+ async #openEntry(name, size) {
19481
+ const destination = this.#request.entries[name];
19482
+ if (destination === void 0 || this.#seen.has(name)) {
19483
+ this.failure = "unexpected-entry";
19484
+ return false;
19485
+ }
19486
+ this.#written += size;
19487
+ if (this.#written > this.#request.maxBytes) {
19488
+ this.failure = "too-large";
19489
+ return false;
19490
+ }
19491
+ this.#pending = name;
19492
+ this.#sink = await open(destination, "wx", 384);
19493
+ this.#remaining = size;
19494
+ this.#padding = (512 - size % 512) % 512;
19495
+ this.#state = size > 0 ? "body" : "padding";
19496
+ return true;
19497
+ }
19498
+ /**
19499
+ * Writes body bytes straight out of the queue.
19500
+ *
19501
+ * Deliberately not routed through {@link TarExtractor.#take}: gathering a contiguous buffer first
19502
+ * would copy the whole executable an extra time on its way to disk, and nothing here needs the
19503
+ * bytes in hand. Only headers and padding, which are one block at a time, do.
19504
+ */
19505
+ async #readBody() {
19506
+ const count = Math.min(this.#remaining, this.#queued);
19507
+ if (count === 0) return false;
19508
+ let written = 0;
19509
+ while (written < count) {
19510
+ const slice = this.#shift(count - written);
19511
+ if (slice === void 0) return false;
19512
+ written += slice.byteLength;
19513
+ await this.#sink?.write(slice);
19514
+ }
19515
+ this.#remaining -= count;
19516
+ if (this.#remaining === 0) this.#state = "padding";
19517
+ return true;
19518
+ }
19519
+ async #endEntry() {
19520
+ if (this.#padding > 0) {
19521
+ if (this.#take(this.#padding) === void 0) return false;
19522
+ this.#padding = 0;
19523
+ }
19524
+ const sink = this.#sink;
19525
+ this.#sink = void 0;
19526
+ await sink?.sync();
19527
+ await sink?.close();
19528
+ if (this.#pending !== void 0) {
19529
+ this.#seen.add(this.#pending);
19530
+ this.#pending = void 0;
19531
+ }
19532
+ this.#state = "header";
19533
+ return true;
19534
+ }
19535
+ /** Up to `limit` bytes off the front of the queue, without copying them. */
19536
+ #shift(limit) {
19537
+ const chunk = this.#chunks[0];
19538
+ if (chunk === void 0) return;
19539
+ if (chunk.byteLength <= limit) {
19540
+ this.#chunks.shift();
19541
+ this.#queued -= chunk.byteLength;
19542
+ return chunk;
19543
+ }
19544
+ this.#chunks[0] = chunk.subarray(limit);
19545
+ this.#queued -= limit;
19546
+ return chunk.subarray(0, limit);
19547
+ }
19548
+ /** Exactly `count` bytes off the front of the queue, or `undefined` until they have arrived. */
19549
+ #take(count) {
19550
+ if (this.#queued < count) return;
19551
+ const out = new Uint8Array(count);
19552
+ let offset = 0;
19553
+ while (offset < count) {
19554
+ const chunk = this.#chunks[0];
19555
+ if (chunk === void 0) return;
19556
+ const needed = count - offset;
19557
+ if (chunk.byteLength <= needed) {
19558
+ out.set(chunk, offset);
19559
+ offset += chunk.byteLength;
19560
+ this.#chunks.shift();
19561
+ } else {
19562
+ out.set(chunk.subarray(0, needed), offset);
19563
+ offset += needed;
19564
+ this.#chunks[0] = chunk.subarray(needed);
19565
+ }
19566
+ }
19567
+ this.#queued -= count;
19568
+ return out;
19569
+ }
19570
+ };
19571
+ //#endregion
19572
+ //#region src/update/stage.ts
19573
+ /**
19574
+ * Turns a validated candidate into a verified executable sitting next to the installed one.
19575
+ *
19576
+ * Nothing here touches the installed binary. Every failure leaves temporary files for the caller
19577
+ * to remove and the running installation exactly as it was — which is the property that makes an
19578
+ * update safe to attempt on every startup.
19579
+ */
19580
+ async function stageExecutable(request) {
19581
+ const download = await request.host.download({
19582
+ destinationPath: request.archivePath,
19583
+ expectedBytes: request.candidate.size,
19584
+ headers: request.downloadHeaders,
19585
+ ...request.onProgress === void 0 ? {} : { onProgress: request.onProgress },
19586
+ timeoutMs: request.downloadTimeoutMs,
19587
+ url: request.candidate.downloadUrl
19588
+ });
19589
+ if (download.kind !== "downloaded") return `download-${download.reason}`;
19590
+ if (download.sha256 !== request.candidate.sha256) return "digest";
19591
+ const archive = await extract(request);
19592
+ if (archive !== void 0) return archive;
19593
+ return await verifyStaged(request);
19594
+ }
19595
+ function extract(request) {
19596
+ return extractArchive({
19597
+ archivePath: request.archivePath,
19598
+ entries: {
19599
+ LICENSE: request.licensePath,
19600
+ [request.entryName]: request.stagedPath
19601
+ },
19602
+ maxBytes: MAX_EXTRACTED_BYTES,
19603
+ requiredEntry: request.entryName
19604
+ });
19605
+ }
19606
+ /**
19607
+ * Proves the staged file is an executable that agrees about which version it is.
19608
+ *
19609
+ * This is the last gate before anything irreversible. A digest proves the bytes match a published
19610
+ * release; running the program proves the release is the one the metadata named, that it starts on
19611
+ * this machine at all, and that its architecture is the one this process is running.
19612
+ */
19613
+ async function verifyStaged(request) {
19614
+ try {
19615
+ await chmod(request.stagedPath, request.executableMode);
19616
+ if (!(await lstat(request.stagedPath)).isFile()) return "staged-version";
19617
+ } catch {
19618
+ return "staged-version";
19619
+ }
19620
+ return await request.host.probeVersion(request.stagedPath, request.probeEnvironment) === request.candidate.version ? void 0 : "staged-version";
19621
+ }
19622
+ //#endregion
19623
+ //#region src/update/install.ts
19624
+ /**
19625
+ * Replaces the running distribution's executable with a newer release, or changes nothing.
19626
+ *
19627
+ * The transaction is ordered so the installed executable is only ever touched by two renames, both
19628
+ * within one directory: a recovery copy moves into place, then the verified staged file moves over
19629
+ * the original. A crash at any earlier point leaves temporary files and nothing else; a crash
19630
+ * between the two renames leaves a working binary and a copy of it.
19631
+ */
19632
+ async function installUpdate(request) {
19633
+ const directory = dirname(request.executablePath);
19634
+ const attempt = await acquireUpdateLock({
19635
+ host: request.host,
19636
+ lockPath: `${request.executablePath}.update-lock`,
19637
+ now: request.now
19638
+ });
19639
+ if (attempt.kind !== "acquired") return attempt.kind === "held" ? { kind: "skipped" } : {
19640
+ kind: "failed",
19641
+ reason: "lock-unavailable"
19642
+ };
19643
+ try {
19644
+ return await transact(request, directory);
19645
+ } catch {
19646
+ return {
19647
+ kind: "failed",
19648
+ reason: "replace-failed"
19649
+ };
19650
+ } finally {
19651
+ await attempt.lock.release();
19652
+ }
19653
+ }
19654
+ async function transact(request, directory) {
19655
+ const installed = await request.host.probeVersion(request.executablePath, request.probeEnvironment);
19656
+ if (installed !== void 0 && !isNewerVersion(request.candidate.version, installed)) return { kind: "skipped" };
19657
+ const executableMode = await regularFileMode(request.executablePath);
19658
+ if (executableMode === void 0) return {
19659
+ kind: "failed",
19660
+ reason: "not-a-regular-file"
19661
+ };
19662
+ const scratch = join(directory, `.${request.command}-${randomUUID()}`);
19663
+ const paths = {
19664
+ archive: `${scratch}.tar.gz`,
19665
+ license: `${scratch}.LICENSE`,
19666
+ previous: `${scratch}.previous`,
19667
+ staged: `${scratch}.staged`
19668
+ };
19669
+ try {
19670
+ const failure = await stageExecutable({
19671
+ archivePath: paths.archive,
19672
+ candidate: request.candidate,
19673
+ downloadHeaders: request.downloadHeaders,
19674
+ downloadTimeoutMs: request.downloadTimeoutMs,
19675
+ entryName: request.command,
19676
+ executableMode,
19677
+ host: request.host,
19678
+ licensePath: paths.license,
19679
+ ...request.onProgress === void 0 ? {} : { onProgress: request.onProgress },
19680
+ probeEnvironment: request.probeEnvironment,
19681
+ stagedPath: paths.staged
19682
+ });
19683
+ if (failure !== void 0) return failure === "digest" ? { kind: "refused" } : {
19684
+ kind: "failed",
19685
+ reason: failure
19686
+ };
19687
+ await swap(request, directory, paths);
19688
+ return { kind: "installed" };
19689
+ } finally {
19690
+ await discard([
19691
+ paths.archive,
19692
+ paths.license,
19693
+ paths.previous,
19694
+ paths.staged
19695
+ ]);
19696
+ }
19697
+ }
19698
+ /** The two renames, in the order that leaves something runnable at every point between them. */
19699
+ async function swap(request, directory, paths) {
19700
+ await retainPrevious(request, paths.previous);
19701
+ await rename(paths.staged, request.executablePath);
19702
+ await replaceLicense(directory, paths.license);
19703
+ await syncDirectory(directory);
19704
+ }
19705
+ /**
19706
+ * Keeps one recovery copy beside the installed binary.
19707
+ *
19708
+ * A hard link costs no space and no read of a hundred-megabyte file; a copy is the fallback for
19709
+ * filesystems that refuse links. The intermediate rename is what makes the copy appear whole: a
19710
+ * reader either sees the previous `.previous` or the new one, never a partially written file.
19711
+ */
19712
+ async function retainPrevious(request, temporary) {
19713
+ try {
19714
+ await link(request.executablePath, temporary);
19715
+ } catch {
19716
+ await copyFile(request.executablePath, temporary);
19717
+ }
19718
+ await rename(temporary, `${request.executablePath}.previous`);
19719
+ }
19720
+ /**
19721
+ * Updates the license text only where the installation already keeps one.
19722
+ *
19723
+ * Replacing a file the original install wrote is maintenance; creating one it never wrote would
19724
+ * put an unexpected file into a directory Aura shares with whatever else lives on the user's path.
19725
+ */
19726
+ async function replaceLicense(directory, staged) {
19727
+ const installed = join(directory, "LICENSE");
19728
+ const mode = await regularFileMode(installed);
19729
+ if (mode === void 0 || !await isRegularFile(staged)) return;
19730
+ await chmod(staged, mode);
19731
+ await rename(staged, installed);
19732
+ }
19733
+ /**
19734
+ * Flushes the directory entry itself where the platform supports it.
19735
+ *
19736
+ * Without this the renames can still be in the filesystem's own buffers after the process exits;
19737
+ * a power loss then leaves a directory that lists neither the old name nor the new one.
19738
+ */
19739
+ async function syncDirectory(directory) {
19740
+ try {
19741
+ const handle = await open(directory, "r");
19742
+ try {
19743
+ await handle.sync();
19744
+ } finally {
19745
+ await handle.close();
19746
+ }
19747
+ } catch {}
19748
+ }
19749
+ async function isRegularFile(path) {
19750
+ return await regularFileMode(path) !== void 0;
19751
+ }
19752
+ async function regularFileMode(path) {
19753
+ try {
19754
+ const status = await lstat(path);
19755
+ return status.isFile() ? status.mode & 511 : void 0;
19756
+ } catch {
19757
+ return;
19758
+ }
19759
+ }
19760
+ async function discard(paths) {
19761
+ for (const path of paths) try {
19762
+ await rm(path, { force: true });
19763
+ } catch {}
19764
+ }
19765
+ //#endregion
19766
+ //#region src/update/metadata.ts
19767
+ /**
19768
+ * Fetches one release-metadata document under the shared caps.
19769
+ *
19770
+ * Both providers ask the same questions of a response — did it arrive, is it a 304, is it a 200 —
19771
+ * and both must answer a credential-bearing request without letting the reason travel with the
19772
+ * result. Keeping that in one place is what stops the two from drifting apart.
19773
+ */
19774
+ async function fetchMetadata(query, options) {
19775
+ const response = await query.httpGet({
19776
+ headers: {
19777
+ accept: options.accept,
19778
+ "user-agent": query.userAgent,
19779
+ ...options.headers,
19780
+ ...query.etag === void 0 ? {} : { "if-none-match": query.etag }
19781
+ },
19782
+ maxResponseBytes: MAX_METADATA_BYTES,
19783
+ timeoutMs: METADATA_TIMEOUT_MS,
19784
+ url: options.url
19785
+ });
19786
+ if (response.kind !== "response") return { kind: "failure" };
19787
+ if (response.status === 304) return { kind: "unchanged" };
19788
+ if (response.status !== 200) return { kind: "failure" };
19789
+ return {
19790
+ body: response.body,
19791
+ ...response.etag === void 0 ? {} : { etag: response.etag },
19792
+ kind: "body"
19793
+ };
19794
+ }
19795
+ /**
19796
+ * The `Authorization` header for a configured credential, or no header at all.
19797
+ *
19798
+ * Built at the call site and never stored: the value leaves scope with the request it authorizes.
19799
+ */
19800
+ function bearer(token) {
19801
+ return token === void 0 ? {} : { authorization: `Bearer ${token}` };
19802
+ }
19803
+ /** Headers the archive download carries. Rebuilt per lookup, so nothing cached holds a secret. */
19804
+ function downloadHeaders(token) {
19805
+ return {
19806
+ accept: "application/octet-stream",
19807
+ ...bearer(token)
19808
+ };
19809
+ }
19810
+ function compareRelease(version, current) {
19811
+ if (version === void 0 || !isInstallableVersion(version)) return { kind: "invalid" };
19812
+ return isNewerVersion(version, current) ? {
19813
+ kind: "newer",
19814
+ version
19815
+ } : { kind: "current" };
19816
+ }
19817
+ /** An entity tag as an optional field, so absence stays absent rather than `undefined`-valued. */
19818
+ function etagField(etag) {
19819
+ return etag === void 0 ? {} : { etag };
19820
+ }
19821
+ /** Reads a source's configured credential, when it names one. */
19822
+ function sourceToken(query, tokenEnvironmentVariable) {
19823
+ return tokenEnvironmentVariable === void 0 ? void 0 : query.readVariable(tokenEnvironmentVariable);
19824
+ }
19825
+ //#endregion
19826
+ //#region src/update/github-release.ts
19827
+ const PUBLIC_API_BASE_URL = "https://api.github.com";
19828
+ const PUBLIC_API_HOST = "api.github.com";
19829
+ const PUBLIC_WEB_ORIGIN = "https://github.com";
19830
+ /**
19831
+ * REST API version this provider is written against.
19832
+ *
19833
+ * Pinned rather than omitted: the response fields the trust decision rests on — `immutable` and
19834
+ * the per-asset `digest` — are the ones a future default version could reshape.
19835
+ */
19836
+ const GITHUB_API_VERSION = "2026-03-10";
19837
+ /**
19838
+ * Resolves the latest release of one GitHub or GitHub Enterprise Server repository.
19839
+ *
19840
+ * The trust boundary is the API's TLS connection plus immutable releases: one authenticated
19841
+ * document supplies the version, the asset, and the digest together, so there is no second
19842
+ * endpoint, moving URL, or long-lived signing key to defend.
19843
+ */
19844
+ async function resolveGitHubRelease(source, query) {
19845
+ const baseUrl = source.apiBaseUrl ?? PUBLIC_API_BASE_URL;
19846
+ if (parseUrl(baseUrl) === void 0) return {
19847
+ kind: "failure",
19848
+ reason: "invalid-release"
19849
+ };
19850
+ const token = sourceToken(query, source.tokenEnvironmentVariable);
19851
+ const response = await fetchMetadata(query, {
19852
+ accept: "application/vnd.github+json",
19853
+ headers: {
19854
+ ...bearer(token),
19855
+ "x-github-api-version": GITHUB_API_VERSION
19856
+ },
19857
+ url: `${trimSlash(baseUrl)}/repos/${source.owner}/${source.repository}/releases/latest`
19858
+ });
19859
+ if (response.kind !== "body") return response.kind === "unchanged" ? {
19860
+ etag: query.etag,
19861
+ kind: "current"
19862
+ } : {
19863
+ kind: "failure",
19864
+ reason: "network"
19865
+ };
19866
+ return narrowRelease(source, query, parseJson(response.body), response.etag, token);
19867
+ }
19868
+ /** Turns the response body into a candidate, refusing every release that is not exactly usable. */
19869
+ function narrowRelease(source, query, document, etag, token) {
19870
+ const release = asRecord(document);
19871
+ if (release === void 0 || !isPublished(release)) return {
19872
+ kind: "failure",
19873
+ reason: "invalid-release"
19874
+ };
19875
+ if (asFlag(release["immutable"]) !== true) return {
19876
+ kind: "failure",
19877
+ reason: "untrusted-release"
19878
+ };
19879
+ const tag = asText(release["tag_name"]) ?? "";
19880
+ const verdict = compareRelease(releaseVersion(tag), query.version);
19881
+ if (verdict.kind !== "newer") return verdict.kind === "current" ? {
19882
+ kind: "current",
19883
+ ...etagField(etag)
19884
+ } : {
19885
+ kind: "failure",
19886
+ reason: "invalid-release"
19887
+ };
19888
+ const asset = selectAsset(source, release["assets"], assetName(query), tag);
19889
+ return asset === void 0 ? {
19890
+ kind: "failure",
19891
+ reason: "invalid-release"
19892
+ } : candidate$1(asset, verdict.version, etag, token);
19893
+ }
19894
+ /**
19895
+ * Whether the release is a published, final one.
19896
+ *
19897
+ * Absent reads as unusable rather than as `false`: a server that stopped reporting draft state must
19898
+ * not have every draft silently promoted to installable.
19899
+ */
19900
+ function isPublished(release) {
19901
+ return asFlag(release["draft"]) === false && asFlag(release["prerelease"]) === false;
19902
+ }
19903
+ /** The version a `v`-prefixed tag names, or `undefined` for any other tag shape. */
19904
+ function releaseVersion(tag) {
19905
+ return tag.startsWith("v") ? tag.slice(1) : void 0;
19906
+ }
19907
+ /** The archive name a release must publish for this target, and the only one accepted. */
19908
+ function assetName(query) {
19909
+ return `${query.command}-${query.target}.tar.gz`;
19910
+ }
19911
+ /**
19912
+ * The candidate, downloading through the API when a token is configured.
19913
+ *
19914
+ * A private repository serves assets only from the API URL with an octet-stream `Accept`, which
19915
+ * answers with the bytes or with a redirect to a temporary signed URL. Public releases keep the
19916
+ * plain browser URL so an unauthenticated download stays unauthenticated.
19917
+ */
19918
+ function candidate$1(asset, version, etag, token) {
19919
+ return {
19920
+ candidate: {
19921
+ downloadUrl: token === void 0 ? asset.browserUrl : asset.apiUrl,
19922
+ sha256: asset.sha256,
19923
+ size: asset.size,
19924
+ version
19925
+ },
19926
+ downloadHeaders: downloadHeaders(token),
19927
+ kind: "candidate",
19928
+ ...etagField(etag)
19929
+ };
19930
+ }
19931
+ /** Selects exactly one target asset and narrows every field the download depends on. */
19932
+ function selectAsset(source, assets, expectedName, tag) {
19933
+ const entries = asArray(assets);
19934
+ if (entries === void 0) return;
19935
+ const matches = entries.map((entry) => asRecord(entry)).filter((entry) => entry !== void 0 && asText(entry["name"]) === expectedName);
19936
+ if (matches.length !== 1) return;
19937
+ return narrowAsset(source, matches[0], expectedName, tag);
19938
+ }
19939
+ function narrowAsset(source, asset, expectedName, tag) {
19940
+ if (asset === void 0) return;
19941
+ const size = asSize(asset["size"], MAX_ARCHIVE_BYTES);
19942
+ const sha256 = asGitHubDigest(asset["digest"]);
19943
+ const browserUrl = asText(asset["browser_download_url"]);
19944
+ const apiUrl = asText(asset["url"]);
19945
+ if (size === void 0 || sha256 === void 0 || browserUrl === void 0) return;
19946
+ if (apiUrl === void 0 || !isExpectedApiUrl(source, apiUrl)) return;
19947
+ if (browserUrl !== expectedBrowserUrl(source, tag, expectedName)) return;
19948
+ return {
19949
+ apiUrl,
19950
+ browserUrl,
19951
+ sha256,
19952
+ size
19953
+ };
19954
+ }
19955
+ function expectedBrowserUrl(source, tag, name) {
19956
+ return `${webOrigin(source)}/${source.owner}/${source.repository}/releases/download/${tag}/${name}`;
19957
+ }
19958
+ function isExpectedApiUrl(source, apiUrl) {
19959
+ const prefix = `${trimSlash(source.apiBaseUrl ?? PUBLIC_API_BASE_URL)}/repos/${source.owner}/${source.repository}/releases/assets/`;
19960
+ return apiUrl.startsWith(prefix) && /^[0-9]+$/u.test(apiUrl.slice(prefix.length));
19961
+ }
19962
+ function webOrigin(source) {
19963
+ const url = parseUrl(source.apiBaseUrl ?? PUBLIC_API_BASE_URL);
19964
+ if (url === void 0) return PUBLIC_WEB_ORIGIN;
19965
+ return url.hostname === PUBLIC_API_HOST ? PUBLIC_WEB_ORIGIN : url.origin;
19966
+ }
19967
+ function parseUrl(raw) {
19968
+ try {
19969
+ const url = new URL(raw);
19970
+ return url.username === "" && url.password === "" ? url : void 0;
19971
+ } catch {
19972
+ return;
19973
+ }
19974
+ }
19975
+ function trimSlash(value) {
19976
+ return value.replace(/\/+$/u, "");
19977
+ }
19978
+ //#endregion
19979
+ //#region src/update/signed-manifest.ts
19980
+ const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
19981
+ const ED25519_KEY_BYTES = 32;
19982
+ const ED25519_SIGNATURE_BYTES = 64;
19983
+ const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+={0,2}$/u;
19984
+ /**
19985
+ * Resolves a release from one signed HTTPS manifest.
19986
+ *
19987
+ * For deployments a GitHub-shaped provider cannot serve honestly — an internal artifact service, or
19988
+ * a GitHub Enterprise Server without immutable releases and per-asset digests. The trust boundary
19989
+ * moves from the transport to the signature, so the manifest URL itself may be a stable "latest"
19990
+ * reference while every asset URL inside it stays pinned to the version it names.
19991
+ */
19992
+ async function resolveSignedManifest(source, query) {
19993
+ const token = sourceToken(query, source.tokenEnvironmentVariable);
19994
+ const response = await fetchMetadata(query, {
19995
+ accept: "application/json",
19996
+ headers: bearer(token),
19997
+ url: source.manifestUrl
19998
+ });
19999
+ if (response.kind !== "body") return response.kind === "unchanged" ? {
20000
+ etag: query.etag,
20001
+ kind: "current"
20002
+ } : {
20003
+ kind: "failure",
20004
+ reason: "network"
20005
+ };
20006
+ return narrowManifest(source, query, response.body, response.etag, token);
20007
+ }
20008
+ function narrowManifest(source, query, body, etag, token) {
20009
+ const payload = verifyEnvelope(parseJson(body), source.trustedPublicKeys);
20010
+ if (payload === void 0) return {
20011
+ kind: "failure",
20012
+ reason: "untrusted-release"
20013
+ };
20014
+ const document = asRecord(parseJson(payload));
20015
+ if (!isFresh(document?.["expiresAt"], query.now)) return {
20016
+ kind: "failure",
20017
+ reason: "stale-manifest"
20018
+ };
20019
+ const verdict = compareRelease(asText(document?.["version"]), query.version);
20020
+ if (verdict.kind !== "newer") return verdict.kind === "current" ? {
20021
+ kind: "current",
20022
+ ...etagField(etag)
20023
+ } : {
20024
+ kind: "failure",
20025
+ reason: "invalid-release"
20026
+ };
20027
+ return candidate(source, query, document?.["assets"], verdict.version, etag, token);
20028
+ }
20029
+ /** The resolved release, with the credential attached only where it is the caller's to send. */
20030
+ function candidate(source, query, assets, version, etag, token) {
20031
+ const archive = manifestAsset(assets, query.target, version);
20032
+ if (archive === void 0) return {
20033
+ kind: "failure",
20034
+ reason: "invalid-release"
20035
+ };
20036
+ return {
20037
+ candidate: {
20038
+ ...archive,
20039
+ version
20040
+ },
20041
+ downloadHeaders: downloadHeaders(sameOrigin(source.manifestUrl, archive.downloadUrl) ? token : void 0),
20042
+ kind: "candidate",
20043
+ ...etagField(etag)
20044
+ };
20045
+ }
20046
+ /**
20047
+ * Whether a signed manifest is still within the window it signed for.
20048
+ *
20049
+ * Required, not optional: a publisher who omits the field would otherwise get a document that is
20050
+ * valid forever, which is exactly the one an attacker wants to keep replaying. Epoch milliseconds
20051
+ * rather than a formatted timestamp, so reading it needs no clock, locale, or calendar.
20052
+ */
20053
+ function isFresh(expiresAt, now) {
20054
+ const expiry = asSize(expiresAt, Number.MAX_SAFE_INTEGER);
20055
+ if (expiry === void 0 || expiry <= now) return false;
20056
+ return expiry - now <= MAX_MANIFEST_FRESHNESS_MS;
20057
+ }
20058
+ function sameOrigin(left, right) {
20059
+ try {
20060
+ return new URL(left).origin === new URL(right).origin;
20061
+ } catch {
20062
+ return false;
20063
+ }
20064
+ }
20065
+ /** Returns the payload only when one trusted key verifies its exact bytes. */
20066
+ function verifyEnvelope(envelope, trustedPublicKeys) {
20067
+ const document = asRecord(envelope);
20068
+ if (document === void 0 || document["schemaVersion"] !== 1) return;
20069
+ const payload = decodeBase64Url(asText(document["payload"]));
20070
+ const signature = decodeBase64Url(asText(document["signature"]));
20071
+ if (payload === void 0 || signature?.byteLength !== ED25519_SIGNATURE_BYTES) return;
20072
+ return trustedPublicKeys.map((key) => publicKey(key)).some((key) => key !== void 0 && verifySignature(key, payload, signature)) ? payload.toString("utf8") : void 0;
20073
+ }
20074
+ function manifestAsset(assets, target, version) {
20075
+ const entry = asRecord(asRecord(assets)?.[target]);
20076
+ const downloadUrl = asText(entry?.["downloadUrl"]);
20077
+ const sha256 = asDigest(entry?.["sha256"]);
20078
+ const size = asSize(entry?.["size"], MAX_ARCHIVE_BYTES);
20079
+ if (downloadUrl === void 0 || sha256 === void 0 || size === void 0) return;
20080
+ return isPinnedUrl(downloadUrl, version) ? {
20081
+ downloadUrl,
20082
+ sha256,
20083
+ size
20084
+ } : void 0;
20085
+ }
20086
+ function isPinnedUrl(raw, version) {
20087
+ try {
20088
+ const url = new URL(raw);
20089
+ return isAllowedHttpUrl(url) && `${url.pathname}${url.search}`.includes(version);
20090
+ } catch {
20091
+ return false;
20092
+ }
20093
+ }
20094
+ function verifySignature(key, payload, signature) {
20095
+ try {
20096
+ return verify(null, payload, key, signature);
20097
+ } catch {
20098
+ return false;
20099
+ }
20100
+ }
20101
+ function publicKey(encoded) {
20102
+ const raw = Buffer.from(encoded, "base64");
20103
+ if (raw.byteLength !== ED25519_KEY_BYTES) return;
20104
+ try {
20105
+ return createPublicKey({
20106
+ format: "der",
20107
+ key: Buffer.concat([ED25519_SPKI_PREFIX, raw]),
20108
+ type: "spki"
20109
+ });
20110
+ } catch {
20111
+ return;
20112
+ }
20113
+ }
20114
+ function decodeBase64Url(value) {
20115
+ return value === void 0 || !BASE64URL_PATTERN.test(value) ? void 0 : Buffer.from(value, "base64url");
20116
+ }
20117
+ //#endregion
20118
+ //#region src/update/provider.ts
20119
+ /** Turns one distribution-specific source into a validated candidate. */
20120
+ function resolveUpdateSource(source, query) {
20121
+ return source.kind === "github-release" ? resolveGitHubRelease(source, query) : resolveSignedManifest(source, query);
20122
+ }
20123
+ /**
20124
+ * The cache key one source's metadata is stored under.
20125
+ *
20126
+ * Includes every build-time field that changes lookup or trust and never includes a credential
20127
+ * value. The complete manifest URL and trusted keys ensure either change selects a new cache entry.
20128
+ */
20129
+ function sourceIdentity(source, command) {
20130
+ if (source.kind === "github-release") return JSON.stringify([
20131
+ "github-release",
20132
+ source.apiBaseUrl ?? "https://api.github.com",
20133
+ source.owner,
20134
+ source.repository,
20135
+ source.tokenEnvironmentVariable ?? "",
20136
+ command
20137
+ ]);
20138
+ return JSON.stringify([
20139
+ "signed-manifest",
20140
+ source.manifestUrl,
20141
+ source.tokenEnvironmentVariable ?? "",
20142
+ source.trustedPublicKeys,
20143
+ command
20144
+ ]);
20145
+ }
20146
+ //#endregion
20147
+ //#region src/update/run.ts
20148
+ /**
20149
+ * Installs a newer release before the requested command runs, or does nothing at all.
20150
+ *
20151
+ * Nothing here can change what the command does or what it exits with. Every failure path is
20152
+ * swallowed: the user asked Aura to check a repository, and an update that could not happen is not
20153
+ * a reason to refuse. The successfully updated executable is used by the next invocation — this
20154
+ * process keeps running the image it started with.
20155
+ *
20156
+ * Swallowed for the user, not for the developer: each refusal names itself through {@link debug},
20157
+ * which writes only when this distribution's debug variable asks it to.
20158
+ */
20159
+ async function runStartupUpdate(request) {
20160
+ const disableEnvironmentVariable = updateEnvironmentVariable(request.branding.command);
20161
+ const debug = createUpdateDebug(disableEnvironmentVariable, request.environmentVariables, request.stderr);
20162
+ try {
20163
+ const verdict = await eligibleInstallation({
20164
+ ...request,
20165
+ disableEnvironmentVariable,
20166
+ version: request.branding.version
20167
+ });
20168
+ if (verdict.kind !== "eligible") {
20169
+ debug(`skipped: ${verdict.reason}`);
20170
+ return;
20171
+ }
20172
+ await check(request, verdict.installation, debug);
20173
+ } catch {
20174
+ debug("skipped: unexpected-error");
20175
+ }
20176
+ }
20177
+ async function check(request, eligible, debug) {
20178
+ const now = request.now().getTime();
20179
+ const identity = sourceIdentity(request.updates, request.branding.command);
20180
+ const entry = await readUpdateCache(request.homeDir, identity, now);
20181
+ if (!shouldCheck(entry, now)) {
20182
+ debug(`skipped: cached-${entry?.outcome ?? "check"}`);
20183
+ return;
20184
+ }
20185
+ const resolution = await resolveUpdateSource(request.updates, {
20186
+ command: request.branding.command,
20187
+ ...entry?.outcome === "current" && entry.etag !== void 0 ? { etag: entry.etag } : {},
20188
+ httpGet: request.httpGet,
20189
+ now,
20190
+ readVariable: (name) => readVariable(request.environmentVariables, name),
20191
+ target: eligible.target,
20192
+ userAgent: `${request.branding.command}/${eligible.version}`,
20193
+ version: eligible.version
20194
+ });
20195
+ debug(resolution.kind === "failure" ? `resolved: ${resolution.reason}` : `resolved: ${resolution.kind}`);
20196
+ await apply(request, eligible, {
20197
+ deadline: now + STARTUP_UPDATE_BUDGET_MS,
20198
+ debug,
20199
+ entry,
20200
+ identity,
20201
+ now,
20202
+ resolution
20203
+ });
20204
+ }
20205
+ async function apply(request, eligible, context) {
20206
+ const { entry, identity, now, resolution } = context;
20207
+ if (resolution.kind === "failure") {
20208
+ await writeUpdateCache(request.homeDir, identity, {
20209
+ checkedAt: now,
20210
+ outcome: "check-failed"
20211
+ });
20212
+ return;
20213
+ }
20214
+ if (resolution.kind !== "candidate") {
20215
+ const etag = resolution.kind === "current" ? resolution.etag ?? entry?.etag : entry?.etag;
20216
+ await writeUpdateCache(request.homeDir, identity, {
20217
+ checkedAt: now,
20218
+ ...etag === void 0 ? {} : { etag },
20219
+ outcome: "current"
20220
+ });
20221
+ return;
20222
+ }
20223
+ const outcome = await install(request, eligible, resolution, context);
20224
+ context.debug(`installed: ${trace(outcome)}`);
20225
+ report(request, eligible, resolution.candidate.version, outcome);
20226
+ await record(request, identity, now, resolution, outcome, attemptsFor(entry, resolution.candidate.version));
20227
+ }
20228
+ /** The transaction, with the progress frame painted around it and always taken back down. */
20229
+ async function install(request, eligible, resolution, context) {
20230
+ const { candidate } = resolution;
20231
+ request.stderr.write(updatingLine(request.branding, eligible.version, candidate.version));
20232
+ const progress = startUpdateProgress(request.stderr);
20233
+ try {
20234
+ return await installUpdate({
20235
+ candidate,
20236
+ command: request.branding.command,
20237
+ downloadHeaders: resolution.downloadHeaders,
20238
+ downloadTimeoutMs: Math.max(0, context.deadline - request.now().getTime()),
20239
+ executablePath: eligible.executablePath,
20240
+ host: request.host,
20241
+ now: context.now,
20242
+ ...progress.report === void 0 ? {} : { onProgress: progress.report },
20243
+ probeEnvironment: probeEnvironment(request)
20244
+ });
20245
+ } finally {
20246
+ progress.close();
20247
+ }
20248
+ }
20249
+ /** The debug trace for one outcome, which is the only place a failure names its reason. */
20250
+ function trace(outcome) {
20251
+ return outcome.kind === "failed" ? `failed: ${outcome.reason}` : outcome.kind;
20252
+ }
20253
+ /** One line, or none: the outcome table's message column. */
20254
+ function report(request, eligible, version, outcome) {
20255
+ if (outcome.kind === "installed") {
20256
+ request.stderr.write(updatedLine(request.branding, version));
20257
+ return;
20258
+ }
20259
+ if (outcome.kind === "refused") {
20260
+ request.stderr.write(digestRefusedLine(request.branding, version));
20261
+ return;
20262
+ }
20263
+ if (outcome.kind === "failed") {
20264
+ const manual = request.updates.manualUpdateUrl ?? request.branding.docsUrl;
20265
+ request.stderr.write(installFailedLine(request.branding, version, manual));
20266
+ }
20267
+ }
20268
+ /** Stores what happened, so a release that will not install is not retried on every command. */
20269
+ function record(request, identity, now, resolution, outcome, attempts) {
20270
+ if (outcome.kind === "installed" || outcome.kind === "skipped") return writeUpdateCache(request.homeDir, identity, {
20271
+ checkedAt: now,
20272
+ outcome: "current"
20273
+ });
20274
+ return writeUpdateCache(request.homeDir, identity, {
20275
+ checkedAt: now,
20276
+ ...resolution.etag === void 0 ? {} : { etag: resolution.etag },
20277
+ failedAttempts: attempts + 1,
20278
+ failedVersion: resolution.candidate.version,
20279
+ outcome: "install-failed"
20280
+ });
20281
+ }
20282
+ /**
20283
+ * The environment a version probe runs in.
20284
+ *
20285
+ * Deliberately tiny, and deliberately carrying this distribution's own disable variable: the
20286
+ * installer verifies a staged binary by running it, and a child that started an update of its own
20287
+ * would recurse into the directory its parent is mid-transaction on.
20288
+ */
20289
+ function probeEnvironment(request) {
20290
+ const home = request.environmentVariables["HOME"];
20291
+ const path = request.environmentVariables["PATH"];
20292
+ return {
20293
+ ...home === void 0 ? {} : { HOME: home },
20294
+ ...path === void 0 ? {} : { PATH: path },
20295
+ NO_COLOR: "1",
20296
+ [updateEnvironmentVariable(request.branding.command)]: "off"
20297
+ };
20298
+ }
20299
+ /** Reads one variable at the moment of use, treating an empty value as unset. */
20300
+ function readVariable(environmentVariables, name) {
20301
+ const value = environmentVariables[name];
20302
+ return value === void 0 || value === "" ? void 0 : value;
20303
+ }
20304
+ function updatingLine(branding, from, to) {
20305
+ return `Updating ${branding.displayName} ${from} -> ${to}...\n`;
20306
+ }
20307
+ function updatedLine(branding, to) {
20308
+ return `Updated ${branding.displayName} to ${to}. The new version will be used on your next run.\n`;
20309
+ }
20310
+ function installFailedLine(branding, version, manualUpdateUrl) {
20311
+ const suffix = manualUpdateUrl === void 0 ? "" : ` Update manually: ${manualUpdateUrl}`;
20312
+ return `${branding.displayName} could not install the ${version} update.${suffix}\n`;
20313
+ }
20314
+ function digestRefusedLine(branding, version) {
20315
+ return `${branding.displayName} refused the ${version} update: the download did not match the release's published SHA-256 digest. Nothing was installed.\n`;
20316
+ }
20317
+ //#endregion
20318
+ //#region src/run.boundary.ts
20319
+ /** Runs one build-time-composed Aura distribution. */
20320
+ function runCli(distro, runtime) {
20321
+ return run(distro, runtime);
20322
+ }
20323
+ /** Runs a compiled standalone distribution that explicitly owns its executable. */
20324
+ function runStandaloneCli(distro, updates, current, runtime) {
20325
+ return run(distro, runtime, {
20326
+ current,
20327
+ updates
20328
+ });
20329
+ }
20330
+ async function run(distro, runtime, startupUpdate) {
20331
+ const resolved = resolveRuntime(runtime, distro.branding);
20332
+ const telemetry = createTelemetryRecorder({
20333
+ distroVersion: distro.branding.version,
20334
+ now: resolved.now,
20335
+ sink: telemetryEnabled(resolved.environmentVariables) ? distro.telemetry : void 0
20336
+ });
20337
+ if (startupUpdate !== void 0) await runStartupUpdate({
20338
+ argv: resolved.argv,
20339
+ branding: distro.branding,
20340
+ current: startupUpdate.current,
20341
+ environmentVariables: resolved.environmentVariables,
20342
+ homeDir: resolved.homeDir,
20343
+ host: UPDATE_HOST,
20344
+ httpGet: resolved.httpGet ?? createHttpGet(),
20345
+ now: resolved.now,
20346
+ stderr: resolved.stderr,
20347
+ stdin: resolved.stdin,
20348
+ stdout: resolved.stdout,
20349
+ updates: startupUpdate.updates
18636
20350
  });
18637
20351
  const verdict = await runResolved(distro, resolved, telemetry);
18638
20352
  await telemetry.flush();
@@ -18760,4 +20474,4 @@ function applyExitCode(exitCode, runtime) {
18760
20474
  runtime.setExitCode(exitCode);
18761
20475
  }
18762
20476
  //#endregion
18763
- export { vetHttpUrl as i, clampHttpTimeout as n, failureReason as r, runCli as t };
20477
+ export { vetHttpUrl as a, failureReason as i, runStandaloneCli as n, clampHttpTimeout as r, runCli as t };