@dahch/heimdall 1.0.1 โ 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.
- package/README.md +9 -6
- package/dist/cli.js +506 -165
- 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
|
-
- ๐ **
|
|
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`.
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
|
|
30
30
|
## ๐ฆ Supported Package Managers
|
|
31
31
|
|
|
32
|
-
Heimdall (`hmd`) supports
|
|
32
|
+
Heimdall (`hmd`) supports 14 package managers across system, runtime, language, and application store categories:
|
|
33
33
|
|
|
34
34
|
| Manager | Icon | Category | Detection Strategy | Update Routine & Resilience Rules |
|
|
35
35
|
| :--- | :---: | :---: | :--- | :--- |
|
|
@@ -44,8 +44,9 @@ Heimdall (`hmd`) supports 13 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
|
|
48
|
-
| **[
|
|
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
|
+
| **[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 --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) |
|
|
49
50
|
|
|
50
51
|
---
|
|
51
52
|
|
|
@@ -156,7 +157,9 @@ Test coverage includes:
|
|
|
156
157
|
- Concurrency pool bounds and error isolation in [`UpdaterEngine`](src/core/engine.ts).
|
|
157
158
|
- `NpmManager` targeted batch installation, fallback trigger handling, and `--ignore-scripts` resilience.
|
|
158
159
|
- `HomebrewManager` selective formula vs. cask execution branches.
|
|
159
|
-
- Pre-check behaviors (skipping `/usr/bin/gem`, sudo privilege checks for MacPorts and
|
|
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.
|
|
160
163
|
- HTTP registry lookup error handling for Yarn and pipx.
|
|
161
164
|
- Formatting renderers (micro-border table, directional version diffs, execution summary metrics).
|
|
162
165
|
|
|
@@ -178,7 +181,7 @@ Heimdall employs an automated GitHub Actions CI/CD workflow ([`.github/workflows
|
|
|
178
181
|
|
|
179
182
|
- [SPEC.md](SPEC.md): Technical specification, CLI contract, data models, and exit codes.
|
|
180
183
|
- [DESIGN.md](DESIGN.md): System architecture, Two-Phase workflow, Engine concurrency pool, and Mermaid diagrams.
|
|
181
|
-
- [ADR.md](ADR.md): Architectural Decision Records (ADR-001 through ADR-
|
|
184
|
+
- [ADR.md](ADR.md): Architectural Decision Records (ADR-001 through ADR-010) documenting key engineering decisions.
|
|
182
185
|
- [AGENTS.md](AGENTS.md): Developer and AI agent reference manual for maintaining and extending the codebase.
|
|
183
186
|
|
|
184
187
|
---
|
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
|
-
|
|
1380
|
-
|
|
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
|
|
1422
|
+
error
|
|
1394
1423
|
}
|
|
1395
1424
|
],
|
|
1396
|
-
error
|
|
1425
|
+
error
|
|
1397
1426
|
};
|
|
1398
1427
|
}
|
|
1399
1428
|
}
|
|
@@ -1513,12 +1542,108 @@ var MacAppStoreManager = class extends BasePackageManager {
|
|
|
1513
1542
|
}
|
|
1514
1543
|
};
|
|
1515
1544
|
|
|
1545
|
+
// src/utils/formatting.ts
|
|
1546
|
+
import pc from "picocolors";
|
|
1547
|
+
import Table from "cli-table3";
|
|
1548
|
+
function formatDuration(ms) {
|
|
1549
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
1550
|
+
const seconds = (ms / 1e3).toFixed(1);
|
|
1551
|
+
return `${seconds}s`;
|
|
1552
|
+
}
|
|
1553
|
+
function renderUpdatesTable(items) {
|
|
1554
|
+
if (items.length === 0) {
|
|
1555
|
+
return pc.dim(" No updates pending.");
|
|
1556
|
+
}
|
|
1557
|
+
const table = new Table({
|
|
1558
|
+
head: [
|
|
1559
|
+
pc.dim("Manager"),
|
|
1560
|
+
pc.dim("Package"),
|
|
1561
|
+
pc.dim("Type"),
|
|
1562
|
+
pc.dim("Version Transition")
|
|
1563
|
+
],
|
|
1564
|
+
style: {
|
|
1565
|
+
head: [],
|
|
1566
|
+
border: ["dim"]
|
|
1567
|
+
},
|
|
1568
|
+
chars: {
|
|
1569
|
+
top: "\u2500",
|
|
1570
|
+
"top-mid": "\u2500",
|
|
1571
|
+
"top-left": "\u250C",
|
|
1572
|
+
"top-right": "\u2510",
|
|
1573
|
+
bottom: "\u2500",
|
|
1574
|
+
"bottom-mid": "\u2500",
|
|
1575
|
+
"bottom-left": "\u2514",
|
|
1576
|
+
"bottom-right": "\u2518",
|
|
1577
|
+
left: "\u2502",
|
|
1578
|
+
"left-mid": "\u251C",
|
|
1579
|
+
mid: "\u2500",
|
|
1580
|
+
"mid-mid": "\u253C",
|
|
1581
|
+
right: "\u2502",
|
|
1582
|
+
"right-mid": "\u2524",
|
|
1583
|
+
middle: "\u2502"
|
|
1584
|
+
}
|
|
1585
|
+
});
|
|
1586
|
+
for (const item of items) {
|
|
1587
|
+
const typeLabel = item.type ? pc.dim(`[${item.type}]`) : pc.dim("-");
|
|
1588
|
+
const currentVer = pc.yellow(item.currentVersion || "?");
|
|
1589
|
+
const arrow = pc.dim(" \u2192 ");
|
|
1590
|
+
const latestVer = pc.green(pc.bold(item.latestVersion || "?"));
|
|
1591
|
+
table.push([
|
|
1592
|
+
pc.cyan(item.managerName),
|
|
1593
|
+
pc.bold(pc.white(item.name)),
|
|
1594
|
+
typeLabel,
|
|
1595
|
+
`${currentVer}${arrow}${latestVer}`
|
|
1596
|
+
]);
|
|
1597
|
+
}
|
|
1598
|
+
return table.toString();
|
|
1599
|
+
}
|
|
1600
|
+
function renderExecutionSummary(results) {
|
|
1601
|
+
const total = results.length;
|
|
1602
|
+
const successful = results.filter((r) => r.success).length;
|
|
1603
|
+
const failed = results.filter((r) => !r.success).length;
|
|
1604
|
+
const totalUpdated = results.reduce((acc, r) => acc + r.updatedCount, 0);
|
|
1605
|
+
const lines = [];
|
|
1606
|
+
lines.push("");
|
|
1607
|
+
lines.push(pc.dim("\u2500".repeat(60)));
|
|
1608
|
+
lines.push(` ${pc.bold(pc.cyan("Heimdall"))} ${pc.dim("\u2022 Execution Summary")}`);
|
|
1609
|
+
lines.push(pc.dim("\u2500".repeat(60)));
|
|
1610
|
+
for (const res of results) {
|
|
1611
|
+
const icon = res.icon || "\u{1F4E6}";
|
|
1612
|
+
const status = res.success ? pc.green("\u2714 SUCCESS") : pc.red("\u2716 FAILED");
|
|
1613
|
+
const time = pc.dim(`(${formatDuration(res.durationMs)})`);
|
|
1614
|
+
const count = res.success ? pc.dim(`\u2022 ${res.updatedCount} updated`) : pc.dim(`\u2022 ${res.updatedCount} updated, ${res.steps.filter((s) => s.status === "failed").length} issue(s)`);
|
|
1615
|
+
lines.push(` ${icon} ${pc.bold(res.managerName)} ${status} ${count} ${time}`);
|
|
1616
|
+
if (!res.success && res.error) {
|
|
1617
|
+
lines.push(` ${pc.red("\u21B3 Error:")} ${pc.dim(res.error.trim().split("\n")[0])}`);
|
|
1618
|
+
}
|
|
1619
|
+
if (res.steps.length > 0) {
|
|
1620
|
+
for (const step of res.steps) {
|
|
1621
|
+
if (step.status === "skipped") continue;
|
|
1622
|
+
const stepStatus = step.status === "success" ? pc.green("\u2714") : step.status === "failed" ? pc.red("\u2716") : pc.dim("\u25CB");
|
|
1623
|
+
const stepDuration = step.durationMs ? pc.dim(`[${formatDuration(step.durationMs)}]`) : "";
|
|
1624
|
+
const stepError = step.error && step.status === "failed" ? pc.red(` (${step.error.split("\n")[0]})`) : "";
|
|
1625
|
+
lines.push(` ${stepStatus} ${pc.dim(step.name)} ${stepDuration}${stepError}`);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
lines.push(pc.dim("\u2500".repeat(60)));
|
|
1630
|
+
const summaryParts = [
|
|
1631
|
+
pc.green(`${successful}/${total} managers succeeded`),
|
|
1632
|
+
failed > 0 ? pc.red(`${failed} failed`) : null,
|
|
1633
|
+
pc.cyan(`${totalUpdated} package(s) updated`)
|
|
1634
|
+
].filter(Boolean);
|
|
1635
|
+
lines.push(` ${summaryParts.join(" \u2022 ")}`);
|
|
1636
|
+
lines.push(pc.dim("\u2500".repeat(60)));
|
|
1637
|
+
return lines.join("\n");
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1516
1640
|
// src/managers/linux.ts
|
|
1517
1641
|
var AptManager = class extends BasePackageManager {
|
|
1518
1642
|
id = "apt";
|
|
1519
1643
|
name = "APT (Debian/Ubuntu)";
|
|
1520
1644
|
icon = "\u{1F427}";
|
|
1521
1645
|
category = "system";
|
|
1646
|
+
requiresSudo = true;
|
|
1522
1647
|
binary = "apt-get";
|
|
1523
1648
|
async checkUpdates(options) {
|
|
1524
1649
|
const startTime = Date.now();
|
|
@@ -1581,8 +1706,8 @@ var AptManager = class extends BasePackageManager {
|
|
|
1581
1706
|
const startTime = Date.now();
|
|
1582
1707
|
const steps = [];
|
|
1583
1708
|
if (!options.dryRun && process.getuid?.() !== 0) {
|
|
1584
|
-
|
|
1585
|
-
|
|
1709
|
+
if (!await this.hasPasswordlessSudo()) {
|
|
1710
|
+
const error = `APT requires sudo privileges. ${SUDO_REQUIRED_HINT}`;
|
|
1586
1711
|
return {
|
|
1587
1712
|
managerId: this.id,
|
|
1588
1713
|
managerName: this.name,
|
|
@@ -1595,10 +1720,10 @@ var AptManager = class extends BasePackageManager {
|
|
|
1595
1720
|
name: "sudo privileges",
|
|
1596
1721
|
command: "sudo -n true",
|
|
1597
1722
|
status: "failed",
|
|
1598
|
-
error
|
|
1723
|
+
error
|
|
1599
1724
|
}
|
|
1600
1725
|
],
|
|
1601
|
-
error
|
|
1726
|
+
error
|
|
1602
1727
|
};
|
|
1603
1728
|
}
|
|
1604
1729
|
}
|
|
@@ -1612,18 +1737,54 @@ var AptManager = class extends BasePackageManager {
|
|
|
1612
1737
|
const upgradeStep = await this.executeStep(
|
|
1613
1738
|
"sudo apt-get upgrade",
|
|
1614
1739
|
"sudo",
|
|
1615
|
-
["apt-get", "upgrade", "-y"],
|
|
1740
|
+
["apt-get", "upgrade", "-y", "--with-new-pkgs"],
|
|
1616
1741
|
options
|
|
1617
1742
|
);
|
|
1618
1743
|
steps.push(upgradeStep);
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
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");
|
|
1627
1788
|
return {
|
|
1628
1789
|
managerId: this.id,
|
|
1629
1790
|
managerName: this.name,
|
|
@@ -1641,6 +1802,7 @@ var FlatpakManager = class extends BasePackageManager {
|
|
|
1641
1802
|
name = "Flatpak";
|
|
1642
1803
|
icon = "\u{1F4E6}";
|
|
1643
1804
|
category = "system";
|
|
1805
|
+
requiresSudo = true;
|
|
1644
1806
|
binary = "flatpak";
|
|
1645
1807
|
async checkUpdates(options) {
|
|
1646
1808
|
const startTime = Date.now();
|
|
@@ -1657,23 +1819,43 @@ var FlatpakManager = class extends BasePackageManager {
|
|
|
1657
1819
|
};
|
|
1658
1820
|
}
|
|
1659
1821
|
try {
|
|
1660
|
-
const
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
const
|
|
1667
|
-
|
|
1668
|
-
const parts = trimmed.split(/\s+/);
|
|
1669
|
-
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 {
|
|
1670
1830
|
managerId: this.id,
|
|
1671
1831
|
managerName: this.name,
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
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
|
+
}
|
|
1677
1859
|
}
|
|
1678
1860
|
return {
|
|
1679
1861
|
managerId: this.id,
|
|
@@ -1697,17 +1879,243 @@ var FlatpakManager = class extends BasePackageManager {
|
|
|
1697
1879
|
};
|
|
1698
1880
|
}
|
|
1699
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
|
+
}
|
|
1700
1888
|
async executeUpdate(items, options) {
|
|
1701
1889
|
const startTime = Date.now();
|
|
1702
1890
|
const steps = [];
|
|
1703
|
-
const
|
|
1704
|
-
"flatpak update
|
|
1891
|
+
const userStep = await this.executeStep(
|
|
1892
|
+
"flatpak update --user",
|
|
1705
1893
|
"flatpak",
|
|
1706
|
-
["update", "-y", "--noninteractive"],
|
|
1894
|
+
["update", "--user", "-y", "--noninteractive"],
|
|
1707
1895
|
options
|
|
1708
1896
|
);
|
|
1709
|
-
steps.push(
|
|
1710
|
-
const
|
|
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");
|
|
1932
|
+
return {
|
|
1933
|
+
managerId: this.id,
|
|
1934
|
+
managerName: this.name,
|
|
1935
|
+
icon: this.icon,
|
|
1936
|
+
success,
|
|
1937
|
+
updatedCount: success ? items.length : 0,
|
|
1938
|
+
durationMs: Date.now() - startTime,
|
|
1939
|
+
steps,
|
|
1940
|
+
error: !success ? steps.find((s) => s.status === "failed")?.error : void 0
|
|
1941
|
+
};
|
|
1942
|
+
}
|
|
1943
|
+
};
|
|
1944
|
+
var DnfManager = class extends BasePackageManager {
|
|
1945
|
+
id = "dnf";
|
|
1946
|
+
name = "DNF (Fedora/RHEL)";
|
|
1947
|
+
icon = "\u{1F4E6}";
|
|
1948
|
+
category = "system";
|
|
1949
|
+
requiresSudo = true;
|
|
1950
|
+
binary = "dnf";
|
|
1951
|
+
async checkUpdates(options) {
|
|
1952
|
+
const startTime = Date.now();
|
|
1953
|
+
const isAvail = await this.isAvailable();
|
|
1954
|
+
if (!isAvail) {
|
|
1955
|
+
return {
|
|
1956
|
+
managerId: this.id,
|
|
1957
|
+
managerName: this.name,
|
|
1958
|
+
icon: this.icon,
|
|
1959
|
+
category: this.category,
|
|
1960
|
+
available: false,
|
|
1961
|
+
updates: [],
|
|
1962
|
+
durationMs: 0
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
try {
|
|
1966
|
+
const execRes = await safeExec(this.binary, ["check-update", "-y"], {
|
|
1967
|
+
timeoutMs: options?.timeoutMs ?? 25e3
|
|
1968
|
+
});
|
|
1969
|
+
if (execRes.exitCode !== 0 && execRes.exitCode !== 100) {
|
|
1970
|
+
return {
|
|
1971
|
+
managerId: this.id,
|
|
1972
|
+
managerName: this.name,
|
|
1973
|
+
icon: this.icon,
|
|
1974
|
+
category: this.category,
|
|
1975
|
+
available: true,
|
|
1976
|
+
updates: [],
|
|
1977
|
+
durationMs: Date.now() - startTime,
|
|
1978
|
+
error: (execRes.stderr || execRes.stdout).trim() ? extractErrorMessage(execRes.stderr || execRes.stdout) : `dnf check-update failed with code ${execRes.exitCode}`
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
if (execRes.exitCode === 0) {
|
|
1982
|
+
return {
|
|
1983
|
+
managerId: this.id,
|
|
1984
|
+
managerName: this.name,
|
|
1985
|
+
icon: this.icon,
|
|
1986
|
+
category: this.category,
|
|
1987
|
+
available: true,
|
|
1988
|
+
updates: [],
|
|
1989
|
+
durationMs: Date.now() - startTime
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
const updates = [];
|
|
1993
|
+
const lines = execRes.stdout.split("\n");
|
|
1994
|
+
let pendingPkg = null;
|
|
1995
|
+
for (const rawLine of lines) {
|
|
1996
|
+
const line = rawLine.trim();
|
|
1997
|
+
if (!line) continue;
|
|
1998
|
+
if (line.startsWith("Updating and loading repositories") || line.startsWith("Repositories loaded") || line.startsWith("Last metadata expiration check") || line.startsWith("Obsoleting Packages") || line.startsWith("Loaded plugins") || line.startsWith("Security:")) {
|
|
1999
|
+
continue;
|
|
2000
|
+
}
|
|
2001
|
+
const combinedLine = pendingPkg ? `${pendingPkg} ${line}` : line;
|
|
2002
|
+
const match = combinedLine.match(/^(\S+?)\.([a-zA-Z0-9_]+)\s+(\S+)\s+(\S+)/);
|
|
2003
|
+
if (match) {
|
|
2004
|
+
const [, name, arch, latestVersion, repo] = match;
|
|
2005
|
+
updates.push({
|
|
2006
|
+
managerId: this.id,
|
|
2007
|
+
managerName: this.name,
|
|
2008
|
+
name,
|
|
2009
|
+
currentVersion: "installed",
|
|
2010
|
+
latestVersion,
|
|
2011
|
+
type: "rpm",
|
|
2012
|
+
extra: `${repo} (${arch})`
|
|
2013
|
+
});
|
|
2014
|
+
pendingPkg = null;
|
|
2015
|
+
} else if (!pendingPkg && /^\S+?\.[a-zA-Z0-9_]+$/.test(line)) {
|
|
2016
|
+
pendingPkg = line;
|
|
2017
|
+
} else {
|
|
2018
|
+
pendingPkg = null;
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
return {
|
|
2022
|
+
managerId: this.id,
|
|
2023
|
+
managerName: this.name,
|
|
2024
|
+
icon: this.icon,
|
|
2025
|
+
category: this.category,
|
|
2026
|
+
available: true,
|
|
2027
|
+
updates,
|
|
2028
|
+
durationMs: Date.now() - startTime
|
|
2029
|
+
};
|
|
2030
|
+
} catch (err) {
|
|
2031
|
+
return {
|
|
2032
|
+
managerId: this.id,
|
|
2033
|
+
managerName: this.name,
|
|
2034
|
+
icon: this.icon,
|
|
2035
|
+
category: this.category,
|
|
2036
|
+
available: true,
|
|
2037
|
+
updates: [],
|
|
2038
|
+
durationMs: Date.now() - startTime,
|
|
2039
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
async executeUpdate(items, options) {
|
|
2044
|
+
const startTime = Date.now();
|
|
2045
|
+
const steps = [];
|
|
2046
|
+
if (!options.dryRun && process.getuid?.() !== 0) {
|
|
2047
|
+
if (!await this.hasPasswordlessSudo()) {
|
|
2048
|
+
const error = `DNF requires sudo privileges. ${SUDO_REQUIRED_HINT}`;
|
|
2049
|
+
return {
|
|
2050
|
+
managerId: this.id,
|
|
2051
|
+
managerName: this.name,
|
|
2052
|
+
icon: this.icon,
|
|
2053
|
+
success: false,
|
|
2054
|
+
updatedCount: 0,
|
|
2055
|
+
durationMs: Date.now() - startTime,
|
|
2056
|
+
steps: [
|
|
2057
|
+
{
|
|
2058
|
+
name: "sudo privileges",
|
|
2059
|
+
command: "sudo -n true",
|
|
2060
|
+
status: "failed",
|
|
2061
|
+
error
|
|
2062
|
+
}
|
|
2063
|
+
],
|
|
2064
|
+
error
|
|
2065
|
+
};
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
const upgradeStep = await this.executeStep(
|
|
2069
|
+
"sudo dnf upgrade --refresh",
|
|
2070
|
+
"sudo",
|
|
2071
|
+
["dnf", "upgrade", "-y", "--refresh"],
|
|
2072
|
+
options
|
|
2073
|
+
);
|
|
2074
|
+
steps.push(upgradeStep);
|
|
2075
|
+
if (upgradeStep.status === "success") {
|
|
2076
|
+
if (options.dryRun) {
|
|
2077
|
+
const autoremoveStep = await this.executeStep(
|
|
2078
|
+
"sudo dnf autoremove",
|
|
2079
|
+
"sudo",
|
|
2080
|
+
["dnf", "autoremove", "-y"],
|
|
2081
|
+
options
|
|
2082
|
+
);
|
|
2083
|
+
steps.push(autoremoveStep);
|
|
2084
|
+
} else if (options.confirmAction) {
|
|
2085
|
+
const shouldAutoremove = await options.confirmAction(
|
|
2086
|
+
"Do you want to run sudo dnf autoremove to clean up orphaned packages?"
|
|
2087
|
+
);
|
|
2088
|
+
if (shouldAutoremove) {
|
|
2089
|
+
const autoremoveStep = await this.executeStep(
|
|
2090
|
+
"sudo dnf autoremove",
|
|
2091
|
+
"sudo",
|
|
2092
|
+
["dnf", "autoremove", "-y"],
|
|
2093
|
+
options
|
|
2094
|
+
);
|
|
2095
|
+
steps.push(autoremoveStep);
|
|
2096
|
+
} else {
|
|
2097
|
+
steps.push({
|
|
2098
|
+
name: "sudo dnf autoremove",
|
|
2099
|
+
command: "sudo dnf autoremove -y",
|
|
2100
|
+
status: "skipped"
|
|
2101
|
+
});
|
|
2102
|
+
}
|
|
2103
|
+
} else {
|
|
2104
|
+
steps.push({
|
|
2105
|
+
name: "sudo dnf autoremove",
|
|
2106
|
+
command: "sudo dnf autoremove -y",
|
|
2107
|
+
status: "skipped"
|
|
2108
|
+
});
|
|
2109
|
+
}
|
|
2110
|
+
} else {
|
|
2111
|
+
steps.push({
|
|
2112
|
+
name: "sudo dnf autoremove",
|
|
2113
|
+
command: "sudo dnf autoremove -y",
|
|
2114
|
+
status: "skipped"
|
|
2115
|
+
});
|
|
2116
|
+
}
|
|
2117
|
+
const nonSkippedSteps = steps.filter((s) => s.status !== "skipped");
|
|
2118
|
+
const success = nonSkippedSteps.length > 0 && nonSkippedSteps.every((s) => s.status === "success");
|
|
1711
2119
|
return {
|
|
1712
2120
|
managerId: this.id,
|
|
1713
2121
|
managerName: this.name,
|
|
@@ -1736,6 +2144,7 @@ function createDefaultManagers() {
|
|
|
1736
2144
|
new MacPortsManager(),
|
|
1737
2145
|
new MacAppStoreManager(),
|
|
1738
2146
|
new AptManager(),
|
|
2147
|
+
new DnfManager(),
|
|
1739
2148
|
new FlatpakManager()
|
|
1740
2149
|
];
|
|
1741
2150
|
}
|
|
@@ -1872,101 +2281,6 @@ var UpdaterEngine = class {
|
|
|
1872
2281
|
}
|
|
1873
2282
|
};
|
|
1874
2283
|
|
|
1875
|
-
// src/utils/formatting.ts
|
|
1876
|
-
import pc from "picocolors";
|
|
1877
|
-
import Table from "cli-table3";
|
|
1878
|
-
function formatDuration(ms) {
|
|
1879
|
-
if (ms < 1e3) return `${ms}ms`;
|
|
1880
|
-
const seconds = (ms / 1e3).toFixed(1);
|
|
1881
|
-
return `${seconds}s`;
|
|
1882
|
-
}
|
|
1883
|
-
function renderUpdatesTable(items) {
|
|
1884
|
-
if (items.length === 0) {
|
|
1885
|
-
return pc.dim(" No updates pending.");
|
|
1886
|
-
}
|
|
1887
|
-
const table = new Table({
|
|
1888
|
-
head: [
|
|
1889
|
-
pc.dim("Manager"),
|
|
1890
|
-
pc.dim("Package"),
|
|
1891
|
-
pc.dim("Type"),
|
|
1892
|
-
pc.dim("Version Transition")
|
|
1893
|
-
],
|
|
1894
|
-
style: {
|
|
1895
|
-
head: [],
|
|
1896
|
-
border: ["dim"]
|
|
1897
|
-
},
|
|
1898
|
-
chars: {
|
|
1899
|
-
top: "\u2500",
|
|
1900
|
-
"top-mid": "\u2500",
|
|
1901
|
-
"top-left": "\u250C",
|
|
1902
|
-
"top-right": "\u2510",
|
|
1903
|
-
bottom: "\u2500",
|
|
1904
|
-
"bottom-mid": "\u2500",
|
|
1905
|
-
"bottom-left": "\u2514",
|
|
1906
|
-
"bottom-right": "\u2518",
|
|
1907
|
-
left: "\u2502",
|
|
1908
|
-
"left-mid": "\u251C",
|
|
1909
|
-
mid: "\u2500",
|
|
1910
|
-
"mid-mid": "\u253C",
|
|
1911
|
-
right: "\u2502",
|
|
1912
|
-
"right-mid": "\u2524",
|
|
1913
|
-
middle: "\u2502"
|
|
1914
|
-
}
|
|
1915
|
-
});
|
|
1916
|
-
for (const item of items) {
|
|
1917
|
-
const typeLabel = item.type ? pc.dim(`[${item.type}]`) : pc.dim("-");
|
|
1918
|
-
const currentVer = pc.yellow(item.currentVersion || "?");
|
|
1919
|
-
const arrow = pc.dim(" \u2192 ");
|
|
1920
|
-
const latestVer = pc.green(pc.bold(item.latestVersion || "?"));
|
|
1921
|
-
table.push([
|
|
1922
|
-
pc.cyan(item.managerName),
|
|
1923
|
-
pc.bold(pc.white(item.name)),
|
|
1924
|
-
typeLabel,
|
|
1925
|
-
`${currentVer}${arrow}${latestVer}`
|
|
1926
|
-
]);
|
|
1927
|
-
}
|
|
1928
|
-
return table.toString();
|
|
1929
|
-
}
|
|
1930
|
-
function renderExecutionSummary(results) {
|
|
1931
|
-
const total = results.length;
|
|
1932
|
-
const successful = results.filter((r) => r.success).length;
|
|
1933
|
-
const failed = results.filter((r) => !r.success).length;
|
|
1934
|
-
const totalUpdated = results.reduce((acc, r) => acc + r.updatedCount, 0);
|
|
1935
|
-
const lines = [];
|
|
1936
|
-
lines.push("");
|
|
1937
|
-
lines.push(pc.dim("\u2500".repeat(60)));
|
|
1938
|
-
lines.push(` ${pc.bold(pc.cyan("Heimdall"))} ${pc.dim("\u2022 Execution Summary")}`);
|
|
1939
|
-
lines.push(pc.dim("\u2500".repeat(60)));
|
|
1940
|
-
for (const res of results) {
|
|
1941
|
-
const icon = res.icon || "\u{1F4E6}";
|
|
1942
|
-
const status = res.success ? pc.green("\u2714 SUCCESS") : pc.red("\u2716 FAILED");
|
|
1943
|
-
const time = pc.dim(`(${formatDuration(res.durationMs)})`);
|
|
1944
|
-
const count = res.success ? pc.dim(`\u2022 ${res.updatedCount} updated`) : pc.dim(`\u2022 ${res.updatedCount} updated, ${res.steps.filter((s) => s.status === "failed").length} issue(s)`);
|
|
1945
|
-
lines.push(` ${icon} ${pc.bold(res.managerName)} ${status} ${count} ${time}`);
|
|
1946
|
-
if (!res.success && res.error) {
|
|
1947
|
-
lines.push(` ${pc.red("\u21B3 Error:")} ${pc.dim(res.error.trim().split("\n")[0])}`);
|
|
1948
|
-
}
|
|
1949
|
-
if (res.steps.length > 0) {
|
|
1950
|
-
for (const step of res.steps) {
|
|
1951
|
-
if (step.status === "skipped") continue;
|
|
1952
|
-
const stepStatus = step.status === "success" ? pc.green("\u2714") : step.status === "failed" ? pc.red("\u2716") : pc.dim("\u25CB");
|
|
1953
|
-
const stepDuration = step.durationMs ? pc.dim(`[${formatDuration(step.durationMs)}]`) : "";
|
|
1954
|
-
const stepError = step.error && step.status === "failed" ? pc.red(` (${step.error.split("\n")[0]})`) : "";
|
|
1955
|
-
lines.push(` ${stepStatus} ${pc.dim(step.name)} ${stepDuration}${stepError}`);
|
|
1956
|
-
}
|
|
1957
|
-
}
|
|
1958
|
-
}
|
|
1959
|
-
lines.push(pc.dim("\u2500".repeat(60)));
|
|
1960
|
-
const summaryParts = [
|
|
1961
|
-
pc.green(`${successful}/${total} managers succeeded`),
|
|
1962
|
-
failed > 0 ? pc.red(`${failed} failed`) : null,
|
|
1963
|
-
pc.cyan(`${totalUpdated} package(s) updated`)
|
|
1964
|
-
].filter(Boolean);
|
|
1965
|
-
lines.push(` ${summaryParts.join(" \u2022 ")}`);
|
|
1966
|
-
lines.push(pc.dim("\u2500".repeat(60)));
|
|
1967
|
-
return lines.join("\n");
|
|
1968
|
-
}
|
|
1969
|
-
|
|
1970
2284
|
// src/utils/version.ts
|
|
1971
2285
|
import { fileURLToPath } from "url";
|
|
1972
2286
|
import { dirname, resolve } from "path";
|
|
@@ -2154,6 +2468,21 @@ ${pc2.bold("Pending updates")} ${pc2.dim(`(${allUpdates.length} total):`)}`);
|
|
|
2154
2468
|
process.exit(0);
|
|
2155
2469
|
}
|
|
2156
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
|
+
}
|
|
2157
2486
|
p.log.step(
|
|
2158
2487
|
pc2.bold(
|
|
2159
2488
|
pc2.cyan(
|
|
@@ -2164,43 +2493,55 @@ ${isSimulated ? "[DRY RUN] Simulating" : "Starting"} updates for ${managersToUpd
|
|
|
2164
2493
|
);
|
|
2165
2494
|
const updateSpinner = p.spinner();
|
|
2166
2495
|
let managerIndex = 0;
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
const
|
|
2179
|
-
updateSpinner.
|
|
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..."));
|
|
2180
2526
|
}
|
|
2181
2527
|
},
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
const detail = error ? pc2.red(` \u21B3 ${error.split("\n")[0]}`) : "";
|
|
2186
|
-
updateSpinner.stop(` ${mark} ${pc2.dim(stepName)}${durStr}${detail}`);
|
|
2187
|
-
updateSpinner.start(pc2.dim("Working..."));
|
|
2188
|
-
}
|
|
2189
|
-
},
|
|
2190
|
-
(mgr) => {
|
|
2191
|
-
managerIndex++;
|
|
2192
|
-
console.log(pc2.bold(pc2.cyan(`
|
|
2528
|
+
(mgr) => {
|
|
2529
|
+
managerIndex++;
|
|
2530
|
+
console.log(pc2.bold(pc2.cyan(`
|
|
2193
2531
|
\u25CF [${managerIndex}/${managersToUpdate.length}] ${mgr.icon} ${mgr.name}`)));
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
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
|
+
}
|
|
2201
2540
|
}
|
|
2202
|
-
|
|
2203
|
-
|
|
2541
|
+
);
|
|
2542
|
+
} finally {
|
|
2543
|
+
stopSudoKeepAlive?.();
|
|
2544
|
+
}
|
|
2204
2545
|
console.log(renderExecutionSummary(summary.results));
|
|
2205
2546
|
if (summary.failedCount === 0) {
|
|
2206
2547
|
p.outro(
|