@dahch/heimdall 1.1.0 โ†’ 1.2.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.
Files changed (3) hide show
  1. package/README.md +6 -4
  2. package/dist/cli.js +228 -81
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -20,7 +20,7 @@
20
20
  2. **Aggregated Review & Confirmation**: Shows an interactive tabular overview comparing current and latest package versions across all managers, prompting for confirmation before modifying the system.
21
21
  - ๐Ÿงช **True Simulated Dry-Run (`--dry-run`)**: Traverses the full engine lifecycle, formats outdated packages, and executes mock update routines step-by-step with real-time feedback without touching system files.
22
22
  - โšก **High-Speed Registry Queries**: Yarn and pipx bypass heavy CLI subprocess spawning by querying the npm Registry and PyPI JSON APIs directly over HTTP with strict 3-second abort timeouts.
23
- - ๐Ÿ” **Non-Interactive Sudo Pre-Checks**: Privileged managers (MacPorts, Linux APT, Linux DNF) verify cached credentials using `sudo -n true` beforehand, preventing terminal lockups and password prompt hangs.
23
+ - ๐Ÿ” **Interactive Sudo Acquisition**: When a selected manager requires elevation (`requiresSudo`), `hmd` verifies the cached ticket with `sudo -n true` and, if missing, prompts for the password exactly once through `sudo -v` on the controlling terminal before any spinner takes over the TTY. A background keep-alive refreshes the ticket during long upgrades, and every privileged step re-verifies non-interactive credentials so subprocesses can never hang on a hidden password prompt. Running `sudo hmd` is neither required nor supported (the CLI lives under the user's `PATH`, which root's `secure_path` does not include).
24
24
  - ๐ŸŽจ **Modern Minimalist UX**: Terminal interface powered by `@clack/prompts`, micro-border tabular layouts with directional version transitions (`current โ†’ latest`), persistent real-time step progression with elapsed durations, and actionable error extraction.
25
25
  - ๐Ÿท **Dynamic Version Resolution**: Reads package name and version dynamically from `package.json` at runtime via `import.meta.url`, ensuring banners and `--version` flags remain in sync with repository releases.
26
26
  - โš™ **Flexible Targeting**: Run interactively, execute non-interactively with `--yes`, or filter target managers using `--only` and `--exclude`.
@@ -44,9 +44,9 @@ Heimdall (`hmd`) supports 14 package managers across system, runtime, language,
44
44
  | **[Ruby Gem](https://rubygems.org)** | ๐Ÿ’Ž | `language` | `gem outdated` (skips deprecated Apple System Ruby `/usr/bin/gem`) | 1. `gem update --user-install`<br>2. `gem cleanup` |
45
45
  | **[MacPorts](https://www.macports.org)** | โš“ | `system` | `port outdated` + non-interactive `sudo -n true` verification | 1. `sudo port selfupdate`<br>2. `sudo port upgrade outdated` |
46
46
  | **[Mac App Store (mas)](https://github.com/mas-cli/mas)** | ๐ŸŽ | `appstore` | `mas outdated` | `mas upgrade` |
47
- | **[APT (Debian/Ubuntu)](https://wiki.debian.org/Apt)** | ๐Ÿง | `system` | `apt list --upgradable` + non-interactive `sudo -n true` check | 1. `sudo apt-get update -y`<br>2. `sudo apt-get upgrade -y`<br>3. `sudo apt-get autoremove -y` |
47
+ | **[APT (Debian/Ubuntu)](https://wiki.debian.org/Apt)** | ๐Ÿง | `system` | `apt list --upgradable` + non-interactive `sudo -n true` check | 1. `sudo apt-get update -y`<br>2. `sudo apt-get upgrade -y --with-new-pkgs` (avoids leaving dependency-gaining upgrades `kept back`)<br>3. `sudo apt-get autoremove -y` (prompted interactively with confirmation prompt, auto-approved with `--yes` or `--dry-run`, skipped when declined or after a failed upgrade) |
48
48
  | **[DNF (Fedora/RHEL)](https://docs.fedoraproject.org/en-US/quick-docs/dnf/)** | ๐Ÿ“ฆ | `system` | `dnf check-update -y` (exit code 100 signals updates) + `sudo -n true` check | 1. `sudo dnf upgrade -y --refresh`<br>2. `sudo dnf autoremove -y` (prompted interactively with confirmation prompt, auto-approved with `--yes` or `--dry-run`) |
49
- | **[Flatpak](https://flatpak.org)** | ๐Ÿ“ฆ | `system` | `flatpak remote-ls --updates` | `flatpak update -y --noninteractive` |
49
+ | **[Flatpak](https://flatpak.org)** | ๐Ÿ“ฆ | `system` | `flatpak remote-ls --user --updates` + `flatpak remote-ls --system --updates` (both scopes, deduplicated) | 1. `flatpak update --user -y --noninteractive`<br>2. `sudo flatpak update --system -y --noninteractive` (marked `failed` with sudo guidance when system apps exist but privileges are unavailable, `skipped` when no system-scope apps are installed) |
50
50
 
51
51
  ---
52
52
 
@@ -157,7 +157,9 @@ Test coverage includes:
157
157
  - Concurrency pool bounds and error isolation in [`UpdaterEngine`](src/core/engine.ts).
158
158
  - `NpmManager` targeted batch installation, fallback trigger handling, and `--ignore-scripts` resilience.
159
159
  - `HomebrewManager` selective formula vs. cask execution branches.
160
- - Pre-check behaviors (skipping `/usr/bin/gem`, sudo privilege checks for MacPorts and APT).
160
+ - Pre-check behaviors (skipping `/usr/bin/gem`, sudo privilege checks for MacPorts, APT and DNF, interactive sudo acquisition with ticket keep-alive).
161
+ - `FlatpakManager` dual-scope querying (user + system installations) and system-step sudo gating.
162
+ - `AptManager` upgradable parsing (epochs, architecture-qualified names, header/warning lines) and its consent-gated autoremove branch.
161
163
  - HTTP registry lookup error handling for Yarn and pipx.
162
164
  - Formatting renderers (micro-border table, directional version diffs, execution summary metrics).
163
165
 
package/dist/cli.js CHANGED
@@ -18,6 +18,26 @@ async function commandExists(command) {
18
18
  return false;
19
19
  }
20
20
  }
21
+ var SUDO_REQUIRED_HINT = `Run "sudo -v" in this terminal and retry ("sudo hmd" will not work: hmd is not in root's PATH).`;
22
+ async function ensureSudo() {
23
+ if (process.getuid?.() === 0) return true;
24
+ const cachedTicket = await safeExec("sudo", ["-n", "true"], { timeoutMs: 5e3 });
25
+ if (cachedTicket.success) return true;
26
+ if (!process.stdin.isTTY || !await commandExists("sudo")) return false;
27
+ try {
28
+ const result = await execa("sudo", ["-v"], { stdio: "inherit", reject: false });
29
+ return result.exitCode === 0;
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+ function startSudoKeepAlive(intervalMs = 6e4) {
35
+ const timer = setInterval(() => {
36
+ void safeExec("sudo", ["-n", "-v"], { timeoutMs: 1e4 });
37
+ }, intervalMs);
38
+ timer.unref();
39
+ return () => clearInterval(timer);
40
+ }
21
41
  async function safeExec(file, args = [], options = {}) {
22
42
  const timeout = options.timeoutMs ?? 3e4;
23
43
  try {
@@ -140,6 +160,14 @@ var BasePackageManager = class {
140
160
  async isAvailable() {
141
161
  return commandExists(this.binary);
142
162
  }
163
+ /**
164
+ * Verifies that sudo can run without a password prompt (cached ticket or NOPASSWD rule),
165
+ * so privileged steps fail fast instead of hanging inside a stdin-less subprocess.
166
+ */
167
+ async hasPasswordlessSudo() {
168
+ const result = await safeExec("sudo", ["-n", "true"], { timeoutMs: 3e3 });
169
+ return result.success;
170
+ }
143
171
  async executeStep(stepName, file, args, options, extraExecOptions) {
144
172
  const startTime = Date.now();
145
173
  options.onStepStart?.(stepName);
@@ -1311,6 +1339,7 @@ var MacPortsManager = class extends BasePackageManager {
1311
1339
  name = "MacPorts";
1312
1340
  icon = "\u2693";
1313
1341
  category = "system";
1342
+ requiresSudo = true;
1314
1343
  binary = "port";
1315
1344
  async checkUpdates(options) {
1316
1345
  const startTime = Date.now();
@@ -1376,8 +1405,8 @@ var MacPortsManager = class extends BasePackageManager {
1376
1405
  const startTime = Date.now();
1377
1406
  const steps = [];
1378
1407
  if (!options.dryRun && process.getuid?.() !== 0) {
1379
- const sudoCheck = await safeExec("sudo", ["-n", "true"], { timeoutMs: 3e3 });
1380
- if (!sudoCheck.success) {
1408
+ if (!await this.hasPasswordlessSudo()) {
1409
+ const error = `MacPorts requires sudo privileges. ${SUDO_REQUIRED_HINT}`;
1381
1410
  return {
1382
1411
  managerId: this.id,
1383
1412
  managerName: this.name,
@@ -1390,10 +1419,10 @@ var MacPortsManager = class extends BasePackageManager {
1390
1419
  name: "sudo privileges",
1391
1420
  command: "sudo -n true",
1392
1421
  status: "failed",
1393
- error: 'MacPorts requires sudo privileges. Run "sudo -v" in your terminal before hmd.'
1422
+ error
1394
1423
  }
1395
1424
  ],
1396
- error: 'MacPorts requires sudo privileges. Run "sudo -v" before running hmd.'
1425
+ error
1397
1426
  };
1398
1427
  }
1399
1428
  }
@@ -1614,6 +1643,7 @@ var AptManager = class extends BasePackageManager {
1614
1643
  name = "APT (Debian/Ubuntu)";
1615
1644
  icon = "\u{1F427}";
1616
1645
  category = "system";
1646
+ requiresSudo = true;
1617
1647
  binary = "apt-get";
1618
1648
  async checkUpdates(options) {
1619
1649
  const startTime = Date.now();
@@ -1676,8 +1706,8 @@ var AptManager = class extends BasePackageManager {
1676
1706
  const startTime = Date.now();
1677
1707
  const steps = [];
1678
1708
  if (!options.dryRun && process.getuid?.() !== 0) {
1679
- const sudoCheck = await safeExec("sudo", ["-n", "true"], { timeoutMs: 3e3 });
1680
- if (!sudoCheck.success) {
1709
+ if (!await this.hasPasswordlessSudo()) {
1710
+ const error = `APT requires sudo privileges. ${SUDO_REQUIRED_HINT}`;
1681
1711
  return {
1682
1712
  managerId: this.id,
1683
1713
  managerName: this.name,
@@ -1690,10 +1720,10 @@ var AptManager = class extends BasePackageManager {
1690
1720
  name: "sudo privileges",
1691
1721
  command: "sudo -n true",
1692
1722
  status: "failed",
1693
- error: 'APT requires sudo privileges. Run "sudo -v" in your terminal before hmd.'
1723
+ error
1694
1724
  }
1695
1725
  ],
1696
- error: 'APT requires sudo privileges. Run "sudo -v" before running hmd.'
1726
+ error
1697
1727
  };
1698
1728
  }
1699
1729
  }
@@ -1707,18 +1737,54 @@ var AptManager = class extends BasePackageManager {
1707
1737
  const upgradeStep = await this.executeStep(
1708
1738
  "sudo apt-get upgrade",
1709
1739
  "sudo",
1710
- ["apt-get", "upgrade", "-y"],
1740
+ ["apt-get", "upgrade", "-y", "--with-new-pkgs"],
1711
1741
  options
1712
1742
  );
1713
1743
  steps.push(upgradeStep);
1714
- const cleanupStep = await this.executeStep(
1715
- "sudo apt-get autoremove",
1716
- "sudo",
1717
- ["apt-get", "autoremove", "-y"],
1718
- options
1719
- );
1720
- steps.push(cleanupStep);
1721
- const success = steps.every((s) => s.status === "success");
1744
+ if (upgradeStep.status === "success") {
1745
+ if (options.dryRun) {
1746
+ const autoremoveStep = await this.executeStep(
1747
+ "sudo apt-get autoremove",
1748
+ "sudo",
1749
+ ["apt-get", "autoremove", "-y"],
1750
+ options
1751
+ );
1752
+ steps.push(autoremoveStep);
1753
+ } else if (options.confirmAction) {
1754
+ const shouldAutoremove = await options.confirmAction(
1755
+ "Do you want to run sudo apt-get autoremove to clean up orphaned packages?"
1756
+ );
1757
+ if (shouldAutoremove) {
1758
+ const autoremoveStep = await this.executeStep(
1759
+ "sudo apt-get autoremove",
1760
+ "sudo",
1761
+ ["apt-get", "autoremove", "-y"],
1762
+ options
1763
+ );
1764
+ steps.push(autoremoveStep);
1765
+ } else {
1766
+ steps.push({
1767
+ name: "sudo apt-get autoremove",
1768
+ command: "sudo apt-get autoremove -y",
1769
+ status: "skipped"
1770
+ });
1771
+ }
1772
+ } else {
1773
+ steps.push({
1774
+ name: "sudo apt-get autoremove",
1775
+ command: "sudo apt-get autoremove -y",
1776
+ status: "skipped"
1777
+ });
1778
+ }
1779
+ } else {
1780
+ steps.push({
1781
+ name: "sudo apt-get autoremove",
1782
+ command: "sudo apt-get autoremove -y",
1783
+ status: "skipped"
1784
+ });
1785
+ }
1786
+ const executedSteps = steps.filter((s) => s.status !== "skipped");
1787
+ const success = executedSteps.length > 0 && executedSteps.every((s) => s.status === "success");
1722
1788
  return {
1723
1789
  managerId: this.id,
1724
1790
  managerName: this.name,
@@ -1736,6 +1802,7 @@ var FlatpakManager = class extends BasePackageManager {
1736
1802
  name = "Flatpak";
1737
1803
  icon = "\u{1F4E6}";
1738
1804
  category = "system";
1805
+ requiresSudo = true;
1739
1806
  binary = "flatpak";
1740
1807
  async checkUpdates(options) {
1741
1808
  const startTime = Date.now();
@@ -1752,23 +1819,43 @@ var FlatpakManager = class extends BasePackageManager {
1752
1819
  };
1753
1820
  }
1754
1821
  try {
1755
- const execRes = await safeExec("flatpak", ["remote-ls", "--updates"], {
1756
- timeoutMs: options?.timeoutMs ?? 2e4
1757
- });
1758
- const updates = [];
1759
- const lines = execRes.stdout.split("\n");
1760
- for (const line of lines) {
1761
- const trimmed = line.trim();
1762
- if (!trimmed) continue;
1763
- const parts = trimmed.split(/\s+/);
1764
- updates.push({
1822
+ const timeoutMs = options?.timeoutMs ?? 2e4;
1823
+ const [userRes, systemRes] = await Promise.all([
1824
+ safeExec("flatpak", ["remote-ls", "--user", "--updates"], { timeoutMs }),
1825
+ safeExec("flatpak", ["remote-ls", "--system", "--updates"], { timeoutMs })
1826
+ ]);
1827
+ if (!userRes.success && !systemRes.success) {
1828
+ const raw = (systemRes.stderr || userRes.stderr).trim();
1829
+ return {
1765
1830
  managerId: this.id,
1766
1831
  managerName: this.name,
1767
- name: parts[0],
1768
- currentVersion: "installed",
1769
- latestVersion: parts[1] ?? "latest",
1770
- type: "flatpak"
1771
- });
1832
+ icon: this.icon,
1833
+ category: this.category,
1834
+ available: true,
1835
+ updates: [],
1836
+ durationMs: Date.now() - startTime,
1837
+ error: raw ? extractErrorMessage(raw) : void 0
1838
+ };
1839
+ }
1840
+ const updates = [];
1841
+ const seen = /* @__PURE__ */ new Set();
1842
+ for (const execRes of [userRes, systemRes]) {
1843
+ for (const line of execRes.stdout.split("\n")) {
1844
+ const trimmed = line.trim();
1845
+ if (!trimmed) continue;
1846
+ const parts = trimmed.split(/\s+/);
1847
+ const name = parts[0];
1848
+ if (seen.has(name)) continue;
1849
+ seen.add(name);
1850
+ updates.push({
1851
+ managerId: this.id,
1852
+ managerName: this.name,
1853
+ name,
1854
+ currentVersion: "installed",
1855
+ latestVersion: parts[1] ?? "latest",
1856
+ type: "flatpak"
1857
+ });
1858
+ }
1772
1859
  }
1773
1860
  return {
1774
1861
  managerId: this.id,
@@ -1792,17 +1879,56 @@ var FlatpakManager = class extends BasePackageManager {
1792
1879
  };
1793
1880
  }
1794
1881
  }
1882
+ async hasSystemScopeApps() {
1883
+ const result = await safeExec("flatpak", ["list", "--system", "--columns=application"], {
1884
+ timeoutMs: 1e4
1885
+ });
1886
+ return result.success && result.stdout.trim().length > 0;
1887
+ }
1795
1888
  async executeUpdate(items, options) {
1796
1889
  const startTime = Date.now();
1797
1890
  const steps = [];
1798
- const updateStep = await this.executeStep(
1799
- "flatpak update -y",
1891
+ const userStep = await this.executeStep(
1892
+ "flatpak update --user",
1800
1893
  "flatpak",
1801
- ["update", "-y", "--noninteractive"],
1894
+ ["update", "--user", "-y", "--noninteractive"],
1802
1895
  options
1803
1896
  );
1804
- steps.push(updateStep);
1805
- const success = steps.every((s) => s.status === "success");
1897
+ steps.push(userStep);
1898
+ const sudoStepName = "sudo flatpak update --system";
1899
+ const sudoStepCommand = "sudo flatpak update --system -y --noninteractive";
1900
+ if (options.dryRun) {
1901
+ const systemStep = await this.executeStep(
1902
+ sudoStepName,
1903
+ "sudo",
1904
+ ["flatpak", "update", "--system", "-y", "--noninteractive"],
1905
+ options
1906
+ );
1907
+ steps.push(systemStep);
1908
+ } else if (process.getuid?.() === 0 || await this.hasPasswordlessSudo()) {
1909
+ const systemStep = await this.executeStep(
1910
+ sudoStepName,
1911
+ "sudo",
1912
+ ["flatpak", "update", "--system", "-y", "--noninteractive"],
1913
+ options
1914
+ );
1915
+ steps.push(systemStep);
1916
+ } else if (await this.hasSystemScopeApps()) {
1917
+ steps.push({
1918
+ name: sudoStepName,
1919
+ command: sudoStepCommand,
1920
+ status: "failed",
1921
+ error: `System-wide Flatpak updates require sudo privileges. ${SUDO_REQUIRED_HINT}`
1922
+ });
1923
+ } else {
1924
+ steps.push({
1925
+ name: sudoStepName,
1926
+ command: sudoStepCommand,
1927
+ status: "skipped"
1928
+ });
1929
+ }
1930
+ const executedSteps = steps.filter((s) => s.status !== "skipped");
1931
+ const success = executedSteps.length > 0 && executedSteps.every((s) => s.status === "success");
1806
1932
  return {
1807
1933
  managerId: this.id,
1808
1934
  managerName: this.name,
@@ -1820,6 +1946,7 @@ var DnfManager = class extends BasePackageManager {
1820
1946
  name = "DNF (Fedora/RHEL)";
1821
1947
  icon = "\u{1F4E6}";
1822
1948
  category = "system";
1949
+ requiresSudo = true;
1823
1950
  binary = "dnf";
1824
1951
  async checkUpdates(options) {
1825
1952
  const startTime = Date.now();
@@ -1917,8 +2044,8 @@ var DnfManager = class extends BasePackageManager {
1917
2044
  const startTime = Date.now();
1918
2045
  const steps = [];
1919
2046
  if (!options.dryRun && process.getuid?.() !== 0) {
1920
- const sudoCheck = await safeExec("sudo", ["-n", "true"], { timeoutMs: 3e3 });
1921
- if (!sudoCheck.success) {
2047
+ if (!await this.hasPasswordlessSudo()) {
2048
+ const error = `DNF requires sudo privileges. ${SUDO_REQUIRED_HINT}`;
1922
2049
  return {
1923
2050
  managerId: this.id,
1924
2051
  managerName: this.name,
@@ -1931,10 +2058,10 @@ var DnfManager = class extends BasePackageManager {
1931
2058
  name: "sudo privileges",
1932
2059
  command: "sudo -n true",
1933
2060
  status: "failed",
1934
- error: 'DNF requires sudo privileges. Run "sudo -v" before running hmd.'
2061
+ error
1935
2062
  }
1936
2063
  ],
1937
- error: 'DNF requires sudo privileges. Run "sudo -v" before running hmd.'
2064
+ error
1938
2065
  };
1939
2066
  }
1940
2067
  }
@@ -2341,6 +2468,21 @@ ${pc2.bold("Pending updates")} ${pc2.dim(`(${allUpdates.length} total):`)}`);
2341
2468
  process.exit(0);
2342
2469
  }
2343
2470
  const isSimulated = options.dryRun ?? false;
2471
+ let stopSudoKeepAlive;
2472
+ const selectedIds = new Set(managersToUpdate);
2473
+ const needsSudo = selectedManagers.some((m) => selectedIds.has(m.id) && m.requiresSudo);
2474
+ if (needsSudo && !isSimulated) {
2475
+ p.log.step(pc2.dim("Requesting sudo privileges required by system package managers..."));
2476
+ if (await ensureSudo()) {
2477
+ stopSudoKeepAlive = startSudoKeepAlive();
2478
+ } else {
2479
+ p.log.warn(
2480
+ pc2.yellow(
2481
+ 'Sudo privileges not granted: system package managers will fail. Run "sudo -v" in this terminal and retry.'
2482
+ )
2483
+ );
2484
+ }
2485
+ }
2344
2486
  p.log.step(
2345
2487
  pc2.bold(
2346
2488
  pc2.cyan(
@@ -2351,50 +2493,55 @@ ${isSimulated ? "[DRY RUN] Simulating" : "Starting"} updates for ${managersToUpd
2351
2493
  );
2352
2494
  const updateSpinner = p.spinner();
2353
2495
  let managerIndex = 0;
2354
- const summary = await engine.execute(
2355
- managersToUpdate,
2356
- itemsByManager,
2357
- {
2358
- verbose: options.verbose,
2359
- dryRun: isSimulated,
2360
- confirmAction: async (message) => {
2361
- if (options.yes) return true;
2362
- updateSpinner.stop(pc2.dim("Waiting for user confirmation..."));
2363
- const confirmed = await p.confirm({ message, initialValue: false });
2364
- updateSpinner.start(pc2.dim("Continuing update..."));
2365
- return typeof confirmed === "boolean" ? confirmed : false;
2366
- },
2367
- onStepStart: (stepName) => {
2368
- updateSpinner.message(pc2.dim(`Running: ${stepName}...`));
2369
- },
2370
- onStepProgress: (stepName, logLine) => {
2371
- if (logLine) {
2372
- const truncated = logLine.length > 55 ? logLine.slice(0, 52) + "..." : logLine;
2373
- updateSpinner.message(pc2.dim(`${stepName} [${truncated}]`));
2496
+ let summary;
2497
+ try {
2498
+ summary = await engine.execute(
2499
+ managersToUpdate,
2500
+ itemsByManager,
2501
+ {
2502
+ verbose: options.verbose,
2503
+ dryRun: isSimulated,
2504
+ confirmAction: async (message) => {
2505
+ if (options.yes) return true;
2506
+ updateSpinner.stop(pc2.dim("Waiting for user confirmation..."));
2507
+ const confirmed = await p.confirm({ message, initialValue: false });
2508
+ updateSpinner.start(pc2.dim("Continuing update..."));
2509
+ return typeof confirmed === "boolean" ? confirmed : false;
2510
+ },
2511
+ onStepStart: (stepName) => {
2512
+ updateSpinner.message(pc2.dim(`Running: ${stepName}...`));
2513
+ },
2514
+ onStepProgress: (stepName, logLine) => {
2515
+ if (logLine) {
2516
+ const truncated = logLine.length > 55 ? logLine.slice(0, 52) + "..." : logLine;
2517
+ updateSpinner.message(pc2.dim(`${stepName} [${truncated}]`));
2518
+ }
2519
+ },
2520
+ onStepEnd: (stepName, success, error, durationMs) => {
2521
+ const mark = success ? pc2.green("\u2714") : pc2.red("\u2716");
2522
+ const durStr = durationMs !== void 0 ? pc2.dim(` [${formatDuration(durationMs)}]`) : "";
2523
+ const detail = error ? pc2.red(` \u21B3 ${error.split("\n")[0]}`) : "";
2524
+ updateSpinner.stop(` ${mark} ${pc2.dim(stepName)}${durStr}${detail}`);
2525
+ updateSpinner.start(pc2.dim("Working..."));
2374
2526
  }
2375
2527
  },
2376
- onStepEnd: (stepName, success, error, durationMs) => {
2377
- const mark = success ? pc2.green("\u2714") : pc2.red("\u2716");
2378
- const durStr = durationMs !== void 0 ? pc2.dim(` [${formatDuration(durationMs)}]`) : "";
2379
- const detail = error ? pc2.red(` \u21B3 ${error.split("\n")[0]}`) : "";
2380
- updateSpinner.stop(` ${mark} ${pc2.dim(stepName)}${durStr}${detail}`);
2381
- updateSpinner.start(pc2.dim("Working..."));
2382
- }
2383
- },
2384
- (mgr) => {
2385
- managerIndex++;
2386
- console.log(pc2.bold(pc2.cyan(`
2528
+ (mgr) => {
2529
+ managerIndex++;
2530
+ console.log(pc2.bold(pc2.cyan(`
2387
2531
  \u25CF [${managerIndex}/${managersToUpdate.length}] ${mgr.icon} ${mgr.name}`)));
2388
- updateSpinner.start(pc2.dim("Starting update routine..."));
2389
- },
2390
- (mgr, res) => {
2391
- if (res.success) {
2392
- updateSpinner.stop(pc2.green(` \u2714 ${mgr.name} completed in ${formatDuration(res.durationMs)}`));
2393
- } else {
2394
- updateSpinner.stop(pc2.red(` \u2716 ${mgr.name} completed with issues in ${formatDuration(res.durationMs)}`));
2532
+ updateSpinner.start(pc2.dim("Starting update routine..."));
2533
+ },
2534
+ (mgr, res) => {
2535
+ if (res.success) {
2536
+ updateSpinner.stop(pc2.green(` \u2714 ${mgr.name} completed in ${formatDuration(res.durationMs)}`));
2537
+ } else {
2538
+ updateSpinner.stop(pc2.red(` \u2716 ${mgr.name} completed with issues in ${formatDuration(res.durationMs)}`));
2539
+ }
2395
2540
  }
2396
- }
2397
- );
2541
+ );
2542
+ } finally {
2543
+ stopSudoKeepAlive?.();
2544
+ }
2398
2545
  console.log(renderExecutionSummary(summary.results));
2399
2546
  if (summary.failedCount === 0) {
2400
2547
  p.outro(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dahch/heimdall",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Heimdall: resilient validator and updater for system and global packages",
5
5
  "type": "module",
6
6
  "publishConfig": {