@dahch/heimdall 1.1.0 → 1.3.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 +55 -7
- package/dist/cli.js +1446 -396
- package/package.json +14 -3
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,46 +160,57 @@ 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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
durationMs: 0
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
const result = await safeExec(file, args, {
|
|
157
|
-
...extraExecOptions,
|
|
158
|
-
timeoutMs: options.timeoutMs ?? 18e4,
|
|
159
|
-
// 3 min per step
|
|
160
|
-
onData: (data) => {
|
|
161
|
-
if (options.onStepProgress) {
|
|
162
|
-
const lines = data.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
163
|
-
if (lines.length > 0) {
|
|
164
|
-
options.onStepProgress(stepName, lines[lines.length - 1]);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
const durationMs = Date.now() - startTime;
|
|
170
|
-
const success = result.success;
|
|
171
|
-
const rawOutput = [result.stderr, result.stdout].filter(Boolean).join("\n").trim();
|
|
172
|
-
const errorMsg = !success ? result.timedOut ? `Step timed out after ${Math.round((options.timeoutMs ?? 18e4) / 1e3)}s` : extractErrorMessage(rawOutput) : void 0;
|
|
173
|
-
options.onStepEnd?.(stepName, success, errorMsg, durationMs);
|
|
172
|
+
return runStep(stepName, file, args, options, extraExecOptions);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
async function runStep(stepName, file, args, options, extraExecOptions) {
|
|
176
|
+
const startTime = Date.now();
|
|
177
|
+
options.onStepStart?.(stepName);
|
|
178
|
+
if (options.dryRun) {
|
|
179
|
+
options.onStepProgress?.(stepName, `[DRY-RUN] Would run: ${file} ${args.join(" ")}`);
|
|
180
|
+
options.onStepEnd?.(stepName, true);
|
|
174
181
|
return {
|
|
175
182
|
name: stepName,
|
|
176
183
|
command: `${file} ${args.join(" ")}`,
|
|
177
|
-
status:
|
|
178
|
-
durationMs
|
|
179
|
-
error: errorMsg
|
|
184
|
+
status: "success",
|
|
185
|
+
durationMs: 0
|
|
180
186
|
};
|
|
181
187
|
}
|
|
182
|
-
|
|
188
|
+
const result = await safeExec(file, args, {
|
|
189
|
+
...extraExecOptions,
|
|
190
|
+
timeoutMs: options.timeoutMs ?? 18e4,
|
|
191
|
+
// 3 min per step
|
|
192
|
+
onData: (data) => {
|
|
193
|
+
if (options.onStepProgress) {
|
|
194
|
+
const lines = data.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
195
|
+
if (lines.length > 0) {
|
|
196
|
+
options.onStepProgress(stepName, lines[lines.length - 1]);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
const durationMs = Date.now() - startTime;
|
|
202
|
+
const success = result.success;
|
|
203
|
+
const rawOutput = [result.stderr, result.stdout].filter(Boolean).join("\n").trim();
|
|
204
|
+
const errorMsg = !success ? result.timedOut ? `Step timed out after ${Math.round((options.timeoutMs ?? 18e4) / 1e3)}s` : extractErrorMessage(rawOutput) : void 0;
|
|
205
|
+
options.onStepEnd?.(stepName, success, errorMsg, durationMs);
|
|
206
|
+
return {
|
|
207
|
+
name: stepName,
|
|
208
|
+
command: `${file} ${args.join(" ")}`,
|
|
209
|
+
status: success ? "success" : "failed",
|
|
210
|
+
durationMs,
|
|
211
|
+
error: errorMsg
|
|
212
|
+
};
|
|
213
|
+
}
|
|
183
214
|
|
|
184
215
|
// src/managers/homebrew.ts
|
|
185
216
|
var HomebrewManager = class extends BasePackageManager {
|
|
@@ -393,15 +424,25 @@ var NpmManager = class extends BasePackageManager {
|
|
|
393
424
|
});
|
|
394
425
|
const updates = [];
|
|
395
426
|
const stdout = execRes.stdout.trim();
|
|
427
|
+
let npmError;
|
|
396
428
|
if (stdout.startsWith("{")) {
|
|
397
429
|
const parsed = JSON.parse(stdout);
|
|
398
430
|
for (const [name, info] of Object.entries(parsed)) {
|
|
431
|
+
if (name === "error" || name === "problems") {
|
|
432
|
+
if (name === "error" && info && typeof info === "object" && "summary" in info) {
|
|
433
|
+
npmError = typeof info.summary === "string" ? info.summary : void 0;
|
|
434
|
+
}
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
if (!info || typeof info !== "object") continue;
|
|
438
|
+
const entry = info;
|
|
439
|
+
if (!entry.current && !entry.latest && !entry.wanted) continue;
|
|
399
440
|
updates.push({
|
|
400
441
|
managerId: this.id,
|
|
401
442
|
managerName: this.name,
|
|
402
443
|
name,
|
|
403
|
-
currentVersion:
|
|
404
|
-
latestVersion:
|
|
444
|
+
currentVersion: entry.current ?? "unknown",
|
|
445
|
+
latestVersion: entry.latest ?? entry.wanted ?? "latest",
|
|
405
446
|
type: "global-pkg"
|
|
406
447
|
});
|
|
407
448
|
}
|
|
@@ -413,7 +454,8 @@ var NpmManager = class extends BasePackageManager {
|
|
|
413
454
|
category: this.category,
|
|
414
455
|
available: true,
|
|
415
456
|
updates,
|
|
416
|
-
durationMs: Date.now() - startTime
|
|
457
|
+
durationMs: Date.now() - startTime,
|
|
458
|
+
error: updates.length === 0 ? npmError : void 0
|
|
417
459
|
};
|
|
418
460
|
} catch (err) {
|
|
419
461
|
return {
|
|
@@ -1074,15 +1116,30 @@ var PipxManager = class extends BasePackageManager {
|
|
|
1074
1116
|
}
|
|
1075
1117
|
};
|
|
1076
1118
|
|
|
1077
|
-
// src/managers/
|
|
1078
|
-
var
|
|
1079
|
-
id = "
|
|
1080
|
-
name = "
|
|
1081
|
-
icon = "\u{
|
|
1119
|
+
// src/managers/uv.ts
|
|
1120
|
+
var UvManager = class extends BasePackageManager {
|
|
1121
|
+
id = "uv";
|
|
1122
|
+
name = "uv (Python tools)";
|
|
1123
|
+
icon = "\u{1F9EA}";
|
|
1082
1124
|
category = "language";
|
|
1083
|
-
binary = "
|
|
1084
|
-
|
|
1085
|
-
|
|
1125
|
+
binary = "uv";
|
|
1126
|
+
/**
|
|
1127
|
+
* Parses `uv tool list` output, e.g.:
|
|
1128
|
+
* black v24.10.0
|
|
1129
|
+
* - black
|
|
1130
|
+
* - blackd
|
|
1131
|
+
*/
|
|
1132
|
+
parseToolList(stdout) {
|
|
1133
|
+
const tools = [];
|
|
1134
|
+
for (const rawLine of stdout.split("\n")) {
|
|
1135
|
+
const line = rawLine.trim();
|
|
1136
|
+
if (!line || line.startsWith("-")) continue;
|
|
1137
|
+
const match = line.match(/^(\S+)\s+v?(\d[0-9A-Za-z.+-]*)(?:\s+\(.*\))?$/);
|
|
1138
|
+
if (match) {
|
|
1139
|
+
tools.push({ name: match[1], version: match[2] });
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
return tools;
|
|
1086
1143
|
}
|
|
1087
1144
|
async checkUpdates(options) {
|
|
1088
1145
|
const startTime = Date.now();
|
|
@@ -1099,49 +1156,36 @@ var CargoManager = class extends BasePackageManager {
|
|
|
1099
1156
|
};
|
|
1100
1157
|
}
|
|
1101
1158
|
try {
|
|
1159
|
+
const execRes = await safeExec("uv", ["tool", "list"], {
|
|
1160
|
+
timeoutMs: options?.timeoutMs ?? 15e3
|
|
1161
|
+
});
|
|
1102
1162
|
const updates = [];
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
name: `rustup (${toolchain})`,
|
|
1116
|
-
currentVersion: "installed",
|
|
1117
|
-
latestVersion: "update-available",
|
|
1118
|
-
type: "toolchain"
|
|
1119
|
-
});
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
if (await commandExists("cargo-install-update")) {
|
|
1124
|
-
const listRes = await safeExec("cargo", ["install-update", "-l"], {
|
|
1125
|
-
timeoutMs: options?.timeoutMs ?? 15e3
|
|
1126
|
-
});
|
|
1127
|
-
const lines = listRes.stdout.split("\n");
|
|
1128
|
-
for (const line of lines) {
|
|
1129
|
-
const match = line.match(/^(\S+)\s+v(\S+)\s+v(\S+)/);
|
|
1130
|
-
if (match) {
|
|
1131
|
-
const [, name, cur, latest] = match;
|
|
1132
|
-
if (cur !== latest) {
|
|
1163
|
+
const tools = execRes.success ? this.parseToolList(execRes.stdout) : [];
|
|
1164
|
+
await Promise.all(
|
|
1165
|
+
tools.map(async ({ name, version }) => {
|
|
1166
|
+
try {
|
|
1167
|
+
const response = await fetch(
|
|
1168
|
+
`https://pypi.org/pypi/${encodeURIComponent(name)}/json`,
|
|
1169
|
+
{ signal: AbortSignal.timeout(3e3) }
|
|
1170
|
+
);
|
|
1171
|
+
if (!response.ok) return;
|
|
1172
|
+
const data = await response.json();
|
|
1173
|
+
const latest = data.info?.version;
|
|
1174
|
+
if (latest && latest !== version) {
|
|
1133
1175
|
updates.push({
|
|
1134
1176
|
managerId: this.id,
|
|
1135
1177
|
managerName: this.name,
|
|
1136
1178
|
name,
|
|
1137
|
-
currentVersion:
|
|
1179
|
+
currentVersion: version,
|
|
1138
1180
|
latestVersion: latest,
|
|
1139
|
-
type: "
|
|
1181
|
+
type: "uv-tool"
|
|
1140
1182
|
});
|
|
1141
1183
|
}
|
|
1184
|
+
} catch {
|
|
1142
1185
|
}
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1186
|
+
})
|
|
1187
|
+
);
|
|
1188
|
+
updates.sort((a, b) => a.name.localeCompare(b.name));
|
|
1145
1189
|
return {
|
|
1146
1190
|
managerId: this.id,
|
|
1147
1191
|
managerName: this.name,
|
|
@@ -1167,26 +1211,29 @@ var CargoManager = class extends BasePackageManager {
|
|
|
1167
1211
|
async executeUpdate(items, options) {
|
|
1168
1212
|
const startTime = Date.now();
|
|
1169
1213
|
const steps = [];
|
|
1170
|
-
if (
|
|
1171
|
-
const
|
|
1172
|
-
|
|
1173
|
-
"
|
|
1174
|
-
["
|
|
1175
|
-
options
|
|
1176
|
-
);
|
|
1177
|
-
steps.push(rustupStep);
|
|
1178
|
-
}
|
|
1179
|
-
if (await commandExists("cargo-install-update")) {
|
|
1180
|
-
const cargoStep = await this.executeStep(
|
|
1181
|
-
"cargo install-update -a",
|
|
1182
|
-
"cargo",
|
|
1183
|
-
["install-update", "-a"],
|
|
1214
|
+
if (items.length > 0) {
|
|
1215
|
+
const upgradeStep = await this.executeStep(
|
|
1216
|
+
`uv tool upgrade --all (${items.length} tools)`,
|
|
1217
|
+
"uv",
|
|
1218
|
+
["tool", "upgrade", "--all"],
|
|
1184
1219
|
options
|
|
1185
1220
|
);
|
|
1186
|
-
steps.push(
|
|
1221
|
+
steps.push(upgradeStep);
|
|
1222
|
+
const pinned = items.filter((i) => {
|
|
1223
|
+
const major = (v) => v.replace(/^v/, "").split(".")[0];
|
|
1224
|
+
return major(i.currentVersion) !== major(i.latestVersion);
|
|
1225
|
+
});
|
|
1226
|
+
for (const item of pinned) {
|
|
1227
|
+
const step = await this.executeStep(
|
|
1228
|
+
`uv tool install ${item.name}@latest`,
|
|
1229
|
+
"uv",
|
|
1230
|
+
["tool", "install", `${item.name}@latest`],
|
|
1231
|
+
options
|
|
1232
|
+
);
|
|
1233
|
+
steps.push(step);
|
|
1234
|
+
}
|
|
1187
1235
|
}
|
|
1188
|
-
const
|
|
1189
|
-
const success = !hasFailedStep;
|
|
1236
|
+
const success = steps.every((s) => s.status === "success");
|
|
1190
1237
|
return {
|
|
1191
1238
|
managerId: this.id,
|
|
1192
1239
|
managerName: this.name,
|
|
@@ -1200,22 +1247,17 @@ var CargoManager = class extends BasePackageManager {
|
|
|
1200
1247
|
}
|
|
1201
1248
|
};
|
|
1202
1249
|
|
|
1203
|
-
// src/managers/
|
|
1204
|
-
var
|
|
1205
|
-
id = "
|
|
1206
|
-
name = "
|
|
1207
|
-
icon = "\u{
|
|
1250
|
+
// src/managers/poetry.ts
|
|
1251
|
+
var PoetryManager = class extends BasePackageManager {
|
|
1252
|
+
id = "poetry";
|
|
1253
|
+
name = "Poetry (self-managed)";
|
|
1254
|
+
icon = "\u{1F4DC}";
|
|
1208
1255
|
category = "language";
|
|
1209
|
-
binary = "
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
const gemPath = whichRes.stdout.trim();
|
|
1215
|
-
if (gemPath === "/usr/bin/gem" || gemPath.startsWith("/System/Library")) {
|
|
1216
|
-
return false;
|
|
1217
|
-
}
|
|
1218
|
-
return true;
|
|
1256
|
+
binary = "poetry";
|
|
1257
|
+
/** Parses `Poetry (version 1.8.5)` and older/newer banner variants. */
|
|
1258
|
+
parseInstalledVersion(stdout) {
|
|
1259
|
+
const match = stdout.match(/version\s+(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)/i);
|
|
1260
|
+
return match?.[1];
|
|
1219
1261
|
}
|
|
1220
1262
|
async checkUpdates(options) {
|
|
1221
1263
|
const startTime = Date.now();
|
|
@@ -1232,24 +1274,31 @@ var RubyGemManager = class extends BasePackageManager {
|
|
|
1232
1274
|
};
|
|
1233
1275
|
}
|
|
1234
1276
|
try {
|
|
1235
|
-
const execRes = await safeExec("
|
|
1236
|
-
timeoutMs: options?.timeoutMs ??
|
|
1277
|
+
const execRes = await safeExec("poetry", ["--version"], {
|
|
1278
|
+
timeoutMs: options?.timeoutMs ?? 15e3
|
|
1237
1279
|
});
|
|
1238
1280
|
const updates = [];
|
|
1239
|
-
const
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
const [, name, current, latest] = match;
|
|
1245
|
-
updates.push({
|
|
1246
|
-
managerId: this.id,
|
|
1247
|
-
managerName: this.name,
|
|
1248
|
-
name: name.trim(),
|
|
1249
|
-
currentVersion: current.trim(),
|
|
1250
|
-
latestVersion: latest.trim(),
|
|
1251
|
-
type: "gem"
|
|
1281
|
+
const current = this.parseInstalledVersion(execRes.stdout);
|
|
1282
|
+
if (current) {
|
|
1283
|
+
try {
|
|
1284
|
+
const response = await fetch("https://pypi.org/pypi/poetry/json", {
|
|
1285
|
+
signal: AbortSignal.timeout(3e3)
|
|
1252
1286
|
});
|
|
1287
|
+
if (response.ok) {
|
|
1288
|
+
const data = await response.json();
|
|
1289
|
+
const latest = data.info?.version;
|
|
1290
|
+
if (latest && latest !== current) {
|
|
1291
|
+
updates.push({
|
|
1292
|
+
managerId: this.id,
|
|
1293
|
+
managerName: this.name,
|
|
1294
|
+
name: "poetry",
|
|
1295
|
+
currentVersion: current,
|
|
1296
|
+
latestVersion: latest,
|
|
1297
|
+
type: "poetry-self"
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
} catch {
|
|
1253
1302
|
}
|
|
1254
1303
|
}
|
|
1255
1304
|
return {
|
|
@@ -1277,20 +1326,15 @@ var RubyGemManager = class extends BasePackageManager {
|
|
|
1277
1326
|
async executeUpdate(items, options) {
|
|
1278
1327
|
const startTime = Date.now();
|
|
1279
1328
|
const steps = [];
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
"gem",
|
|
1290
|
-
["cleanup"],
|
|
1291
|
-
options
|
|
1292
|
-
);
|
|
1293
|
-
steps.push(cleanupStep);
|
|
1329
|
+
if (items.length > 0) {
|
|
1330
|
+
const step = await this.executeStep(
|
|
1331
|
+
"poetry self update",
|
|
1332
|
+
"poetry",
|
|
1333
|
+
["self", "update"],
|
|
1334
|
+
options
|
|
1335
|
+
);
|
|
1336
|
+
steps.push(step);
|
|
1337
|
+
}
|
|
1294
1338
|
const success = steps.every((s) => s.status === "success");
|
|
1295
1339
|
return {
|
|
1296
1340
|
managerId: this.id,
|
|
@@ -1305,81 +1349,526 @@ var RubyGemManager = class extends BasePackageManager {
|
|
|
1305
1349
|
}
|
|
1306
1350
|
};
|
|
1307
1351
|
|
|
1308
|
-
// src/
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
return {
|
|
1320
|
-
managerId: this.id,
|
|
1321
|
-
managerName: this.name,
|
|
1322
|
-
icon: this.icon,
|
|
1323
|
-
category: this.category,
|
|
1324
|
-
available: false,
|
|
1325
|
-
updates: [],
|
|
1326
|
-
durationMs: 0
|
|
1327
|
-
};
|
|
1328
|
-
}
|
|
1329
|
-
try {
|
|
1330
|
-
const execRes = await safeExec("port", ["outdated"], {
|
|
1331
|
-
timeoutMs: options?.timeoutMs ?? 2e4
|
|
1332
|
-
});
|
|
1333
|
-
const updates = [];
|
|
1334
|
-
const lines = execRes.stdout.split("\n");
|
|
1335
|
-
for (const line of lines) {
|
|
1336
|
-
const trimmed = line.trim();
|
|
1337
|
-
if (!trimmed || trimmed.startsWith("The following") || trimmed.startsWith("--")) {
|
|
1338
|
-
continue;
|
|
1339
|
-
}
|
|
1340
|
-
const match = trimmed.match(/^(\S+)\s+(\S+)\s*<\s*(\S+)/);
|
|
1341
|
-
if (match) {
|
|
1342
|
-
const [, name, current, latest] = match;
|
|
1343
|
-
updates.push({
|
|
1344
|
-
managerId: this.id,
|
|
1345
|
-
managerName: this.name,
|
|
1346
|
-
name,
|
|
1347
|
-
currentVersion: current,
|
|
1348
|
-
latestVersion: latest,
|
|
1349
|
-
type: "port"
|
|
1350
|
-
});
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
return {
|
|
1354
|
-
managerId: this.id,
|
|
1355
|
-
managerName: this.name,
|
|
1356
|
-
icon: this.icon,
|
|
1357
|
-
category: this.category,
|
|
1358
|
-
available: true,
|
|
1359
|
-
updates,
|
|
1360
|
-
durationMs: Date.now() - startTime
|
|
1361
|
-
};
|
|
1362
|
-
} catch (err) {
|
|
1363
|
-
return {
|
|
1364
|
-
managerId: this.id,
|
|
1365
|
-
managerName: this.name,
|
|
1366
|
-
icon: this.icon,
|
|
1367
|
-
category: this.category,
|
|
1368
|
-
available: true,
|
|
1369
|
-
updates: [],
|
|
1370
|
-
durationMs: Date.now() - startTime,
|
|
1371
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1372
|
-
};
|
|
1373
|
-
}
|
|
1352
|
+
// src/utils/formatting.ts
|
|
1353
|
+
import pc from "picocolors";
|
|
1354
|
+
import Table from "cli-table3";
|
|
1355
|
+
function formatDuration(ms) {
|
|
1356
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
1357
|
+
const seconds = (ms / 1e3).toFixed(1);
|
|
1358
|
+
return `${seconds}s`;
|
|
1359
|
+
}
|
|
1360
|
+
function renderUpdatesTable(items) {
|
|
1361
|
+
if (items.length === 0) {
|
|
1362
|
+
return pc.dim(" No updates pending.");
|
|
1374
1363
|
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1364
|
+
const table = new Table({
|
|
1365
|
+
head: [
|
|
1366
|
+
pc.dim("Manager"),
|
|
1367
|
+
pc.dim("Package"),
|
|
1368
|
+
pc.dim("Type"),
|
|
1369
|
+
pc.dim("Version Transition")
|
|
1370
|
+
],
|
|
1371
|
+
style: {
|
|
1372
|
+
head: [],
|
|
1373
|
+
border: ["dim"]
|
|
1374
|
+
},
|
|
1375
|
+
chars: {
|
|
1376
|
+
top: "\u2500",
|
|
1377
|
+
"top-mid": "\u2500",
|
|
1378
|
+
"top-left": "\u250C",
|
|
1379
|
+
"top-right": "\u2510",
|
|
1380
|
+
bottom: "\u2500",
|
|
1381
|
+
"bottom-mid": "\u2500",
|
|
1382
|
+
"bottom-left": "\u2514",
|
|
1383
|
+
"bottom-right": "\u2518",
|
|
1384
|
+
left: "\u2502",
|
|
1385
|
+
"left-mid": "\u251C",
|
|
1386
|
+
mid: "\u2500",
|
|
1387
|
+
"mid-mid": "\u253C",
|
|
1388
|
+
right: "\u2502",
|
|
1389
|
+
"right-mid": "\u2524",
|
|
1390
|
+
middle: "\u2502"
|
|
1391
|
+
}
|
|
1392
|
+
});
|
|
1393
|
+
for (const item of items) {
|
|
1394
|
+
const typeLabel = item.type ? pc.dim(`[${item.type}]`) : pc.dim("-");
|
|
1395
|
+
const currentVer = pc.yellow(item.currentVersion || "?");
|
|
1396
|
+
const arrow = pc.dim(" \u2192 ");
|
|
1397
|
+
const latestVer = pc.green(pc.bold(item.latestVersion || "?"));
|
|
1398
|
+
table.push([
|
|
1399
|
+
pc.cyan(item.managerName),
|
|
1400
|
+
pc.bold(pc.white(item.name)),
|
|
1401
|
+
typeLabel,
|
|
1402
|
+
`${currentVer}${arrow}${latestVer}`
|
|
1403
|
+
]);
|
|
1404
|
+
}
|
|
1405
|
+
return table.toString();
|
|
1406
|
+
}
|
|
1407
|
+
function renderExecutionSummary(results) {
|
|
1408
|
+
const total = results.length;
|
|
1409
|
+
const successful = results.filter((r) => r.success).length;
|
|
1410
|
+
const failed = results.filter((r) => !r.success).length;
|
|
1411
|
+
const totalUpdated = results.reduce((acc, r) => acc + r.updatedCount, 0);
|
|
1412
|
+
const lines = [];
|
|
1413
|
+
lines.push("");
|
|
1414
|
+
lines.push(pc.dim("\u2500".repeat(60)));
|
|
1415
|
+
lines.push(` ${pc.bold(pc.cyan("Heimdall"))} ${pc.dim("\u2022 Execution Summary")}`);
|
|
1416
|
+
lines.push(pc.dim("\u2500".repeat(60)));
|
|
1417
|
+
for (const res of results) {
|
|
1418
|
+
const icon = res.icon || "\u{1F4E6}";
|
|
1419
|
+
const status = res.success ? pc.green("\u2714 SUCCESS") : pc.red("\u2716 FAILED");
|
|
1420
|
+
const time = pc.dim(`(${formatDuration(res.durationMs)})`);
|
|
1421
|
+
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)`);
|
|
1422
|
+
lines.push(` ${icon} ${pc.bold(res.managerName)} ${status} ${count} ${time}`);
|
|
1423
|
+
if (!res.success && res.error) {
|
|
1424
|
+
lines.push(` ${pc.red("\u21B3 Error:")} ${pc.dim(res.error.trim().split("\n")[0])}`);
|
|
1425
|
+
}
|
|
1426
|
+
if (res.steps.length > 0) {
|
|
1427
|
+
for (const step of res.steps) {
|
|
1428
|
+
if (step.status === "skipped") continue;
|
|
1429
|
+
const stepStatus = step.status === "success" ? pc.green("\u2714") : step.status === "failed" ? pc.red("\u2716") : pc.dim("\u25CB");
|
|
1430
|
+
const stepDuration = step.durationMs ? pc.dim(`[${formatDuration(step.durationMs)}]`) : "";
|
|
1431
|
+
const stepError = step.error && step.status === "failed" ? pc.red(` (${step.error.split("\n")[0]})`) : "";
|
|
1432
|
+
lines.push(` ${stepStatus} ${pc.dim(step.name)} ${stepDuration}${stepError}`);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
lines.push(pc.dim("\u2500".repeat(60)));
|
|
1437
|
+
const summaryParts = [
|
|
1438
|
+
pc.green(`${successful}/${total} managers succeeded`),
|
|
1439
|
+
failed > 0 ? pc.red(`${failed} failed`) : null,
|
|
1440
|
+
pc.cyan(`${totalUpdated} package(s) updated`)
|
|
1441
|
+
].filter(Boolean);
|
|
1442
|
+
lines.push(` ${summaryParts.join(" \u2022 ")}`);
|
|
1443
|
+
lines.push(pc.dim("\u2500".repeat(60)));
|
|
1444
|
+
return lines.join("\n");
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
// src/managers/conda.ts
|
|
1448
|
+
var CondaManager = class extends BasePackageManager {
|
|
1449
|
+
id = "conda";
|
|
1450
|
+
name = "Conda (Anaconda/Miniforge)";
|
|
1451
|
+
icon = "\u{1F152}";
|
|
1452
|
+
category = "language";
|
|
1453
|
+
binary = "conda";
|
|
1454
|
+
parseDryRun(stdout) {
|
|
1455
|
+
const jsonStart = stdout.indexOf("{");
|
|
1456
|
+
const jsonEnd = stdout.lastIndexOf("}");
|
|
1457
|
+
if (jsonStart === -1 || jsonEnd === -1 || jsonEnd <= jsonStart) return [];
|
|
1458
|
+
let parsed;
|
|
1459
|
+
try {
|
|
1460
|
+
parsed = JSON.parse(stdout.slice(jsonStart, jsonEnd + 1));
|
|
1461
|
+
} catch {
|
|
1462
|
+
return [];
|
|
1463
|
+
}
|
|
1464
|
+
const unlinked = /* @__PURE__ */ new Map();
|
|
1465
|
+
for (const entry of parsed.actions?.UNLINK ?? []) {
|
|
1466
|
+
if (entry.name) unlinked.set(entry.name, entry);
|
|
1467
|
+
}
|
|
1468
|
+
const updates = [];
|
|
1469
|
+
for (const entry of parsed.actions?.LINK ?? []) {
|
|
1470
|
+
if (!entry.name || !entry.version) continue;
|
|
1471
|
+
const previous = unlinked.get(entry.name);
|
|
1472
|
+
if (!previous) continue;
|
|
1473
|
+
updates.push({
|
|
1474
|
+
name: entry.name,
|
|
1475
|
+
currentVersion: previous.version,
|
|
1476
|
+
latestVersion: entry.version,
|
|
1477
|
+
channel: entry.channel
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
return updates;
|
|
1481
|
+
}
|
|
1482
|
+
async checkUpdates(options) {
|
|
1483
|
+
const startTime = Date.now();
|
|
1484
|
+
const isAvail = await this.isAvailable();
|
|
1485
|
+
if (!isAvail) {
|
|
1486
|
+
return {
|
|
1487
|
+
managerId: this.id,
|
|
1488
|
+
managerName: this.name,
|
|
1489
|
+
icon: this.icon,
|
|
1490
|
+
category: this.category,
|
|
1491
|
+
available: false,
|
|
1492
|
+
updates: [],
|
|
1493
|
+
durationMs: 0
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
try {
|
|
1497
|
+
const timeoutMs = Math.max(options?.timeoutMs ?? 6e4, 6e4);
|
|
1498
|
+
const execRes = await safeExec("conda", ["update", "--all", "--dry-run", "--json"], {
|
|
1499
|
+
timeoutMs
|
|
1500
|
+
});
|
|
1501
|
+
const updates = [];
|
|
1502
|
+
if (execRes.success) {
|
|
1503
|
+
for (const action of this.parseDryRun(execRes.stdout)) {
|
|
1504
|
+
updates.push({
|
|
1505
|
+
managerId: this.id,
|
|
1506
|
+
managerName: this.name,
|
|
1507
|
+
name: action.name,
|
|
1508
|
+
currentVersion: action.currentVersion ?? "installed",
|
|
1509
|
+
latestVersion: action.latestVersion,
|
|
1510
|
+
type: "conda-pkg",
|
|
1511
|
+
extra: action.channel
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
const rawError = (execRes.stderr || execRes.stdout).trim();
|
|
1516
|
+
return {
|
|
1517
|
+
managerId: this.id,
|
|
1518
|
+
managerName: this.name,
|
|
1519
|
+
icon: this.icon,
|
|
1520
|
+
category: this.category,
|
|
1521
|
+
available: true,
|
|
1522
|
+
updates,
|
|
1523
|
+
durationMs: Date.now() - startTime,
|
|
1524
|
+
error: !execRes.success && rawError ? extractErrorMessage(rawError) : void 0
|
|
1525
|
+
};
|
|
1526
|
+
} catch (err) {
|
|
1527
|
+
return {
|
|
1528
|
+
managerId: this.id,
|
|
1529
|
+
managerName: this.name,
|
|
1530
|
+
icon: this.icon,
|
|
1531
|
+
category: this.category,
|
|
1532
|
+
available: true,
|
|
1533
|
+
updates: [],
|
|
1534
|
+
durationMs: Date.now() - startTime,
|
|
1535
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
async executeUpdate(items, options) {
|
|
1540
|
+
const startTime = Date.now();
|
|
1541
|
+
const steps = [];
|
|
1542
|
+
if (items.length > 0) {
|
|
1543
|
+
const step = await this.executeStep(
|
|
1544
|
+
`conda update --all (${items.length} pkgs)`,
|
|
1545
|
+
"conda",
|
|
1546
|
+
["update", "--all", "-y"],
|
|
1547
|
+
options
|
|
1548
|
+
);
|
|
1549
|
+
steps.push(step);
|
|
1550
|
+
}
|
|
1551
|
+
const success = steps.every((s) => s.status === "success");
|
|
1552
|
+
return {
|
|
1553
|
+
managerId: this.id,
|
|
1554
|
+
managerName: this.name,
|
|
1555
|
+
icon: this.icon,
|
|
1556
|
+
success,
|
|
1557
|
+
updatedCount: success ? items.length : 0,
|
|
1558
|
+
durationMs: Date.now() - startTime,
|
|
1559
|
+
steps,
|
|
1560
|
+
error: !success ? steps.find((s) => s.status === "failed")?.error : void 0
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
};
|
|
1564
|
+
|
|
1565
|
+
// src/managers/cargo.ts
|
|
1566
|
+
var CargoManager = class extends BasePackageManager {
|
|
1567
|
+
id = "cargo";
|
|
1568
|
+
name = "Rust (Cargo & Rustup)";
|
|
1569
|
+
icon = "\u{1F980}";
|
|
1570
|
+
category = "language";
|
|
1571
|
+
binary = "cargo";
|
|
1572
|
+
async isAvailable() {
|
|
1573
|
+
return await commandExists("cargo") || await commandExists("rustup");
|
|
1574
|
+
}
|
|
1575
|
+
async checkUpdates(options) {
|
|
1576
|
+
const startTime = Date.now();
|
|
1577
|
+
const isAvail = await this.isAvailable();
|
|
1578
|
+
if (!isAvail) {
|
|
1579
|
+
return {
|
|
1580
|
+
managerId: this.id,
|
|
1581
|
+
managerName: this.name,
|
|
1582
|
+
icon: this.icon,
|
|
1583
|
+
category: this.category,
|
|
1584
|
+
available: false,
|
|
1585
|
+
updates: [],
|
|
1586
|
+
durationMs: 0
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
try {
|
|
1590
|
+
const updates = [];
|
|
1591
|
+
if (await commandExists("rustup")) {
|
|
1592
|
+
const checkRes = await safeExec("rustup", ["check"], {
|
|
1593
|
+
timeoutMs: options?.timeoutMs ?? 15e3
|
|
1594
|
+
});
|
|
1595
|
+
const lines = checkRes.stdout.split("\n");
|
|
1596
|
+
for (const line of lines) {
|
|
1597
|
+
if (line.includes("Update available")) {
|
|
1598
|
+
const parts = line.trim().split(/\s+/);
|
|
1599
|
+
const toolchain = parts[0] ?? "toolchain";
|
|
1600
|
+
updates.push({
|
|
1601
|
+
managerId: this.id,
|
|
1602
|
+
managerName: this.name,
|
|
1603
|
+
name: `rustup (${toolchain})`,
|
|
1604
|
+
currentVersion: "installed",
|
|
1605
|
+
latestVersion: "update-available",
|
|
1606
|
+
type: "toolchain"
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
if (await commandExists("cargo-install-update")) {
|
|
1612
|
+
const listRes = await safeExec("cargo", ["install-update", "-l"], {
|
|
1613
|
+
timeoutMs: options?.timeoutMs ?? 15e3
|
|
1614
|
+
});
|
|
1615
|
+
const lines = listRes.stdout.split("\n");
|
|
1616
|
+
for (const line of lines) {
|
|
1617
|
+
const match = line.match(/^(\S+)\s+v(\S+)\s+v(\S+)/);
|
|
1618
|
+
if (match) {
|
|
1619
|
+
const [, name, cur, latest] = match;
|
|
1620
|
+
if (cur !== latest) {
|
|
1621
|
+
updates.push({
|
|
1622
|
+
managerId: this.id,
|
|
1623
|
+
managerName: this.name,
|
|
1624
|
+
name,
|
|
1625
|
+
currentVersion: cur,
|
|
1626
|
+
latestVersion: latest,
|
|
1627
|
+
type: "crate"
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
return {
|
|
1634
|
+
managerId: this.id,
|
|
1635
|
+
managerName: this.name,
|
|
1636
|
+
icon: this.icon,
|
|
1637
|
+
category: this.category,
|
|
1638
|
+
available: true,
|
|
1639
|
+
updates,
|
|
1640
|
+
durationMs: Date.now() - startTime
|
|
1641
|
+
};
|
|
1642
|
+
} catch (err) {
|
|
1643
|
+
return {
|
|
1644
|
+
managerId: this.id,
|
|
1645
|
+
managerName: this.name,
|
|
1646
|
+
icon: this.icon,
|
|
1647
|
+
category: this.category,
|
|
1648
|
+
available: true,
|
|
1649
|
+
updates: [],
|
|
1650
|
+
durationMs: Date.now() - startTime,
|
|
1651
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
async executeUpdate(items, options) {
|
|
1656
|
+
const startTime = Date.now();
|
|
1657
|
+
const steps = [];
|
|
1658
|
+
if (await commandExists("rustup")) {
|
|
1659
|
+
const rustupStep = await this.executeStep(
|
|
1660
|
+
"rustup update",
|
|
1661
|
+
"rustup",
|
|
1662
|
+
["update"],
|
|
1663
|
+
options
|
|
1664
|
+
);
|
|
1665
|
+
steps.push(rustupStep);
|
|
1666
|
+
}
|
|
1667
|
+
if (await commandExists("cargo-install-update")) {
|
|
1668
|
+
const cargoStep = await this.executeStep(
|
|
1669
|
+
"cargo install-update -a",
|
|
1670
|
+
"cargo",
|
|
1671
|
+
["install-update", "-a"],
|
|
1672
|
+
options
|
|
1673
|
+
);
|
|
1674
|
+
steps.push(cargoStep);
|
|
1675
|
+
}
|
|
1676
|
+
const hasFailedStep = steps.some((s) => s.status === "failed");
|
|
1677
|
+
const success = !hasFailedStep;
|
|
1678
|
+
return {
|
|
1679
|
+
managerId: this.id,
|
|
1680
|
+
managerName: this.name,
|
|
1681
|
+
icon: this.icon,
|
|
1682
|
+
success,
|
|
1683
|
+
updatedCount: success ? items.length : 0,
|
|
1684
|
+
durationMs: Date.now() - startTime,
|
|
1685
|
+
steps,
|
|
1686
|
+
error: !success ? steps.find((s) => s.status === "failed")?.error : void 0
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1689
|
+
};
|
|
1690
|
+
|
|
1691
|
+
// src/managers/gem.ts
|
|
1692
|
+
var RubyGemManager = class extends BasePackageManager {
|
|
1693
|
+
id = "gem";
|
|
1694
|
+
name = "Ruby Gem";
|
|
1695
|
+
icon = "\u{1F48E}";
|
|
1696
|
+
category = "language";
|
|
1697
|
+
binary = "gem";
|
|
1698
|
+
async isAvailable() {
|
|
1699
|
+
const hasCmd = await commandExists(this.binary);
|
|
1700
|
+
if (!hasCmd) return false;
|
|
1701
|
+
const whichRes = await safeExec("which", ["gem"], { timeoutMs: 3e3 });
|
|
1702
|
+
const gemPath = whichRes.stdout.trim();
|
|
1703
|
+
if (gemPath === "/usr/bin/gem" || gemPath.startsWith("/System/Library")) {
|
|
1704
|
+
return false;
|
|
1705
|
+
}
|
|
1706
|
+
return true;
|
|
1707
|
+
}
|
|
1708
|
+
async checkUpdates(options) {
|
|
1709
|
+
const startTime = Date.now();
|
|
1710
|
+
const isAvail = await this.isAvailable();
|
|
1711
|
+
if (!isAvail) {
|
|
1712
|
+
return {
|
|
1713
|
+
managerId: this.id,
|
|
1714
|
+
managerName: this.name,
|
|
1715
|
+
icon: this.icon,
|
|
1716
|
+
category: this.category,
|
|
1717
|
+
available: false,
|
|
1718
|
+
updates: [],
|
|
1719
|
+
durationMs: 0
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
try {
|
|
1723
|
+
const execRes = await safeExec("gem", ["outdated"], {
|
|
1724
|
+
timeoutMs: options?.timeoutMs ?? 2e4
|
|
1725
|
+
});
|
|
1726
|
+
const updates = [];
|
|
1727
|
+
const lines = execRes.stdout.split("\n");
|
|
1728
|
+
for (const line of lines) {
|
|
1729
|
+
const trimmed = line.trim();
|
|
1730
|
+
const match = trimmed.match(/^(\S+)\s*\(([^<]+)<\s*([^)]+)\)/);
|
|
1731
|
+
if (match) {
|
|
1732
|
+
const [, name, current, latest] = match;
|
|
1733
|
+
updates.push({
|
|
1734
|
+
managerId: this.id,
|
|
1735
|
+
managerName: this.name,
|
|
1736
|
+
name: name.trim(),
|
|
1737
|
+
currentVersion: current.trim(),
|
|
1738
|
+
latestVersion: latest.trim(),
|
|
1739
|
+
type: "gem"
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
return {
|
|
1744
|
+
managerId: this.id,
|
|
1745
|
+
managerName: this.name,
|
|
1746
|
+
icon: this.icon,
|
|
1747
|
+
category: this.category,
|
|
1748
|
+
available: true,
|
|
1749
|
+
updates,
|
|
1750
|
+
durationMs: Date.now() - startTime
|
|
1751
|
+
};
|
|
1752
|
+
} catch (err) {
|
|
1753
|
+
return {
|
|
1754
|
+
managerId: this.id,
|
|
1755
|
+
managerName: this.name,
|
|
1756
|
+
icon: this.icon,
|
|
1757
|
+
category: this.category,
|
|
1758
|
+
available: true,
|
|
1759
|
+
updates: [],
|
|
1760
|
+
durationMs: Date.now() - startTime,
|
|
1761
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1762
|
+
};
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
async executeUpdate(items, options) {
|
|
1766
|
+
const startTime = Date.now();
|
|
1767
|
+
const steps = [];
|
|
1768
|
+
const updateStep = await this.executeStep(
|
|
1769
|
+
"gem update --user-install",
|
|
1770
|
+
"gem",
|
|
1771
|
+
["update", "--user-install"],
|
|
1772
|
+
options
|
|
1773
|
+
);
|
|
1774
|
+
steps.push(updateStep);
|
|
1775
|
+
const cleanupStep = await this.executeStep(
|
|
1776
|
+
"gem cleanup",
|
|
1777
|
+
"gem",
|
|
1778
|
+
["cleanup"],
|
|
1779
|
+
options
|
|
1780
|
+
);
|
|
1781
|
+
steps.push(cleanupStep);
|
|
1782
|
+
const success = steps.every((s) => s.status === "success");
|
|
1783
|
+
return {
|
|
1784
|
+
managerId: this.id,
|
|
1785
|
+
managerName: this.name,
|
|
1786
|
+
icon: this.icon,
|
|
1787
|
+
success,
|
|
1788
|
+
updatedCount: success ? items.length : 0,
|
|
1789
|
+
durationMs: Date.now() - startTime,
|
|
1790
|
+
steps,
|
|
1791
|
+
error: !success ? steps.find((s) => s.status === "failed")?.error : void 0
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
};
|
|
1795
|
+
|
|
1796
|
+
// src/managers/macports.ts
|
|
1797
|
+
var MacPortsManager = class extends BasePackageManager {
|
|
1798
|
+
id = "macports";
|
|
1799
|
+
name = "MacPorts";
|
|
1800
|
+
icon = "\u2693";
|
|
1801
|
+
category = "system";
|
|
1802
|
+
requiresSudo = true;
|
|
1803
|
+
binary = "port";
|
|
1804
|
+
async checkUpdates(options) {
|
|
1805
|
+
const startTime = Date.now();
|
|
1806
|
+
const isAvail = await this.isAvailable();
|
|
1807
|
+
if (!isAvail) {
|
|
1808
|
+
return {
|
|
1809
|
+
managerId: this.id,
|
|
1810
|
+
managerName: this.name,
|
|
1811
|
+
icon: this.icon,
|
|
1812
|
+
category: this.category,
|
|
1813
|
+
available: false,
|
|
1814
|
+
updates: [],
|
|
1815
|
+
durationMs: 0
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1818
|
+
try {
|
|
1819
|
+
const execRes = await safeExec("port", ["outdated"], {
|
|
1820
|
+
timeoutMs: options?.timeoutMs ?? 2e4
|
|
1821
|
+
});
|
|
1822
|
+
const updates = [];
|
|
1823
|
+
const lines = execRes.stdout.split("\n");
|
|
1824
|
+
for (const line of lines) {
|
|
1825
|
+
const trimmed = line.trim();
|
|
1826
|
+
if (!trimmed || trimmed.startsWith("The following") || trimmed.startsWith("--")) {
|
|
1827
|
+
continue;
|
|
1828
|
+
}
|
|
1829
|
+
const match = trimmed.match(/^(\S+)\s+(\S+)\s*<\s*(\S+)/);
|
|
1830
|
+
if (match) {
|
|
1831
|
+
const [, name, current, latest] = match;
|
|
1832
|
+
updates.push({
|
|
1833
|
+
managerId: this.id,
|
|
1834
|
+
managerName: this.name,
|
|
1835
|
+
name,
|
|
1836
|
+
currentVersion: current,
|
|
1837
|
+
latestVersion: latest,
|
|
1838
|
+
type: "port"
|
|
1839
|
+
});
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
return {
|
|
1843
|
+
managerId: this.id,
|
|
1844
|
+
managerName: this.name,
|
|
1845
|
+
icon: this.icon,
|
|
1846
|
+
category: this.category,
|
|
1847
|
+
available: true,
|
|
1848
|
+
updates,
|
|
1849
|
+
durationMs: Date.now() - startTime
|
|
1850
|
+
};
|
|
1851
|
+
} catch (err) {
|
|
1852
|
+
return {
|
|
1853
|
+
managerId: this.id,
|
|
1854
|
+
managerName: this.name,
|
|
1855
|
+
icon: this.icon,
|
|
1856
|
+
category: this.category,
|
|
1857
|
+
available: true,
|
|
1858
|
+
updates: [],
|
|
1859
|
+
durationMs: Date.now() - startTime,
|
|
1860
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1861
|
+
};
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
async executeUpdate(items, options) {
|
|
1865
|
+
const startTime = Date.now();
|
|
1866
|
+
const steps = [];
|
|
1867
|
+
if (!options.dryRun && process.getuid?.() !== 0) {
|
|
1868
|
+
if (!await this.hasPasswordlessSudo()) {
|
|
1869
|
+
const error = `MacPorts requires sudo privileges. ${SUDO_REQUIRED_HINT}`;
|
|
1870
|
+
return {
|
|
1871
|
+
managerId: this.id,
|
|
1383
1872
|
managerName: this.name,
|
|
1384
1873
|
icon: this.icon,
|
|
1385
1874
|
success: false,
|
|
@@ -1390,10 +1879,10 @@ var MacPortsManager = class extends BasePackageManager {
|
|
|
1390
1879
|
name: "sudo privileges",
|
|
1391
1880
|
command: "sudo -n true",
|
|
1392
1881
|
status: "failed",
|
|
1393
|
-
error
|
|
1882
|
+
error
|
|
1394
1883
|
}
|
|
1395
1884
|
],
|
|
1396
|
-
error
|
|
1885
|
+
error
|
|
1397
1886
|
};
|
|
1398
1887
|
}
|
|
1399
1888
|
}
|
|
@@ -1513,107 +2002,13 @@ var MacAppStoreManager = class extends BasePackageManager {
|
|
|
1513
2002
|
}
|
|
1514
2003
|
};
|
|
1515
2004
|
|
|
1516
|
-
// src/utils/formatting.ts
|
|
1517
|
-
import pc from "picocolors";
|
|
1518
|
-
import Table from "cli-table3";
|
|
1519
|
-
function formatDuration(ms) {
|
|
1520
|
-
if (ms < 1e3) return `${ms}ms`;
|
|
1521
|
-
const seconds = (ms / 1e3).toFixed(1);
|
|
1522
|
-
return `${seconds}s`;
|
|
1523
|
-
}
|
|
1524
|
-
function renderUpdatesTable(items) {
|
|
1525
|
-
if (items.length === 0) {
|
|
1526
|
-
return pc.dim(" No updates pending.");
|
|
1527
|
-
}
|
|
1528
|
-
const table = new Table({
|
|
1529
|
-
head: [
|
|
1530
|
-
pc.dim("Manager"),
|
|
1531
|
-
pc.dim("Package"),
|
|
1532
|
-
pc.dim("Type"),
|
|
1533
|
-
pc.dim("Version Transition")
|
|
1534
|
-
],
|
|
1535
|
-
style: {
|
|
1536
|
-
head: [],
|
|
1537
|
-
border: ["dim"]
|
|
1538
|
-
},
|
|
1539
|
-
chars: {
|
|
1540
|
-
top: "\u2500",
|
|
1541
|
-
"top-mid": "\u2500",
|
|
1542
|
-
"top-left": "\u250C",
|
|
1543
|
-
"top-right": "\u2510",
|
|
1544
|
-
bottom: "\u2500",
|
|
1545
|
-
"bottom-mid": "\u2500",
|
|
1546
|
-
"bottom-left": "\u2514",
|
|
1547
|
-
"bottom-right": "\u2518",
|
|
1548
|
-
left: "\u2502",
|
|
1549
|
-
"left-mid": "\u251C",
|
|
1550
|
-
mid: "\u2500",
|
|
1551
|
-
"mid-mid": "\u253C",
|
|
1552
|
-
right: "\u2502",
|
|
1553
|
-
"right-mid": "\u2524",
|
|
1554
|
-
middle: "\u2502"
|
|
1555
|
-
}
|
|
1556
|
-
});
|
|
1557
|
-
for (const item of items) {
|
|
1558
|
-
const typeLabel = item.type ? pc.dim(`[${item.type}]`) : pc.dim("-");
|
|
1559
|
-
const currentVer = pc.yellow(item.currentVersion || "?");
|
|
1560
|
-
const arrow = pc.dim(" \u2192 ");
|
|
1561
|
-
const latestVer = pc.green(pc.bold(item.latestVersion || "?"));
|
|
1562
|
-
table.push([
|
|
1563
|
-
pc.cyan(item.managerName),
|
|
1564
|
-
pc.bold(pc.white(item.name)),
|
|
1565
|
-
typeLabel,
|
|
1566
|
-
`${currentVer}${arrow}${latestVer}`
|
|
1567
|
-
]);
|
|
1568
|
-
}
|
|
1569
|
-
return table.toString();
|
|
1570
|
-
}
|
|
1571
|
-
function renderExecutionSummary(results) {
|
|
1572
|
-
const total = results.length;
|
|
1573
|
-
const successful = results.filter((r) => r.success).length;
|
|
1574
|
-
const failed = results.filter((r) => !r.success).length;
|
|
1575
|
-
const totalUpdated = results.reduce((acc, r) => acc + r.updatedCount, 0);
|
|
1576
|
-
const lines = [];
|
|
1577
|
-
lines.push("");
|
|
1578
|
-
lines.push(pc.dim("\u2500".repeat(60)));
|
|
1579
|
-
lines.push(` ${pc.bold(pc.cyan("Heimdall"))} ${pc.dim("\u2022 Execution Summary")}`);
|
|
1580
|
-
lines.push(pc.dim("\u2500".repeat(60)));
|
|
1581
|
-
for (const res of results) {
|
|
1582
|
-
const icon = res.icon || "\u{1F4E6}";
|
|
1583
|
-
const status = res.success ? pc.green("\u2714 SUCCESS") : pc.red("\u2716 FAILED");
|
|
1584
|
-
const time = pc.dim(`(${formatDuration(res.durationMs)})`);
|
|
1585
|
-
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)`);
|
|
1586
|
-
lines.push(` ${icon} ${pc.bold(res.managerName)} ${status} ${count} ${time}`);
|
|
1587
|
-
if (!res.success && res.error) {
|
|
1588
|
-
lines.push(` ${pc.red("\u21B3 Error:")} ${pc.dim(res.error.trim().split("\n")[0])}`);
|
|
1589
|
-
}
|
|
1590
|
-
if (res.steps.length > 0) {
|
|
1591
|
-
for (const step of res.steps) {
|
|
1592
|
-
if (step.status === "skipped") continue;
|
|
1593
|
-
const stepStatus = step.status === "success" ? pc.green("\u2714") : step.status === "failed" ? pc.red("\u2716") : pc.dim("\u25CB");
|
|
1594
|
-
const stepDuration = step.durationMs ? pc.dim(`[${formatDuration(step.durationMs)}]`) : "";
|
|
1595
|
-
const stepError = step.error && step.status === "failed" ? pc.red(` (${step.error.split("\n")[0]})`) : "";
|
|
1596
|
-
lines.push(` ${stepStatus} ${pc.dim(step.name)} ${stepDuration}${stepError}`);
|
|
1597
|
-
}
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
lines.push(pc.dim("\u2500".repeat(60)));
|
|
1601
|
-
const summaryParts = [
|
|
1602
|
-
pc.green(`${successful}/${total} managers succeeded`),
|
|
1603
|
-
failed > 0 ? pc.red(`${failed} failed`) : null,
|
|
1604
|
-
pc.cyan(`${totalUpdated} package(s) updated`)
|
|
1605
|
-
].filter(Boolean);
|
|
1606
|
-
lines.push(` ${summaryParts.join(" \u2022 ")}`);
|
|
1607
|
-
lines.push(pc.dim("\u2500".repeat(60)));
|
|
1608
|
-
return lines.join("\n");
|
|
1609
|
-
}
|
|
1610
|
-
|
|
1611
2005
|
// src/managers/linux.ts
|
|
1612
2006
|
var AptManager = class extends BasePackageManager {
|
|
1613
2007
|
id = "apt";
|
|
1614
2008
|
name = "APT (Debian/Ubuntu)";
|
|
1615
2009
|
icon = "\u{1F427}";
|
|
1616
2010
|
category = "system";
|
|
2011
|
+
requiresSudo = true;
|
|
1617
2012
|
binary = "apt-get";
|
|
1618
2013
|
async checkUpdates(options) {
|
|
1619
2014
|
const startTime = Date.now();
|
|
@@ -1676,8 +2071,8 @@ var AptManager = class extends BasePackageManager {
|
|
|
1676
2071
|
const startTime = Date.now();
|
|
1677
2072
|
const steps = [];
|
|
1678
2073
|
if (!options.dryRun && process.getuid?.() !== 0) {
|
|
1679
|
-
|
|
1680
|
-
|
|
2074
|
+
if (!await this.hasPasswordlessSudo()) {
|
|
2075
|
+
const error = `APT requires sudo privileges. ${SUDO_REQUIRED_HINT}`;
|
|
1681
2076
|
return {
|
|
1682
2077
|
managerId: this.id,
|
|
1683
2078
|
managerName: this.name,
|
|
@@ -1690,10 +2085,10 @@ var AptManager = class extends BasePackageManager {
|
|
|
1690
2085
|
name: "sudo privileges",
|
|
1691
2086
|
command: "sudo -n true",
|
|
1692
2087
|
status: "failed",
|
|
1693
|
-
error
|
|
2088
|
+
error
|
|
1694
2089
|
}
|
|
1695
2090
|
],
|
|
1696
|
-
error
|
|
2091
|
+
error
|
|
1697
2092
|
};
|
|
1698
2093
|
}
|
|
1699
2094
|
}
|
|
@@ -1707,18 +2102,54 @@ var AptManager = class extends BasePackageManager {
|
|
|
1707
2102
|
const upgradeStep = await this.executeStep(
|
|
1708
2103
|
"sudo apt-get upgrade",
|
|
1709
2104
|
"sudo",
|
|
1710
|
-
["apt-get", "upgrade", "-y"],
|
|
2105
|
+
["apt-get", "upgrade", "-y", "--with-new-pkgs"],
|
|
1711
2106
|
options
|
|
1712
2107
|
);
|
|
1713
2108
|
steps.push(upgradeStep);
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
2109
|
+
if (upgradeStep.status === "success") {
|
|
2110
|
+
if (options.dryRun) {
|
|
2111
|
+
const autoremoveStep = await this.executeStep(
|
|
2112
|
+
"sudo apt-get autoremove",
|
|
2113
|
+
"sudo",
|
|
2114
|
+
["apt-get", "autoremove", "-y"],
|
|
2115
|
+
options
|
|
2116
|
+
);
|
|
2117
|
+
steps.push(autoremoveStep);
|
|
2118
|
+
} else if (options.confirmAction) {
|
|
2119
|
+
const shouldAutoremove = await options.confirmAction(
|
|
2120
|
+
"Do you want to run sudo apt-get autoremove to clean up orphaned packages?"
|
|
2121
|
+
);
|
|
2122
|
+
if (shouldAutoremove) {
|
|
2123
|
+
const autoremoveStep = await this.executeStep(
|
|
2124
|
+
"sudo apt-get autoremove",
|
|
2125
|
+
"sudo",
|
|
2126
|
+
["apt-get", "autoremove", "-y"],
|
|
2127
|
+
options
|
|
2128
|
+
);
|
|
2129
|
+
steps.push(autoremoveStep);
|
|
2130
|
+
} else {
|
|
2131
|
+
steps.push({
|
|
2132
|
+
name: "sudo apt-get autoremove",
|
|
2133
|
+
command: "sudo apt-get autoremove -y",
|
|
2134
|
+
status: "skipped"
|
|
2135
|
+
});
|
|
2136
|
+
}
|
|
2137
|
+
} else {
|
|
2138
|
+
steps.push({
|
|
2139
|
+
name: "sudo apt-get autoremove",
|
|
2140
|
+
command: "sudo apt-get autoremove -y",
|
|
2141
|
+
status: "skipped"
|
|
2142
|
+
});
|
|
2143
|
+
}
|
|
2144
|
+
} else {
|
|
2145
|
+
steps.push({
|
|
2146
|
+
name: "sudo apt-get autoremove",
|
|
2147
|
+
command: "sudo apt-get autoremove -y",
|
|
2148
|
+
status: "skipped"
|
|
2149
|
+
});
|
|
2150
|
+
}
|
|
2151
|
+
const executedSteps = steps.filter((s) => s.status !== "skipped");
|
|
2152
|
+
const success = executedSteps.length > 0 && executedSteps.every((s) => s.status === "success");
|
|
1722
2153
|
return {
|
|
1723
2154
|
managerId: this.id,
|
|
1724
2155
|
managerName: this.name,
|
|
@@ -1736,6 +2167,7 @@ var FlatpakManager = class extends BasePackageManager {
|
|
|
1736
2167
|
name = "Flatpak";
|
|
1737
2168
|
icon = "\u{1F4E6}";
|
|
1738
2169
|
category = "system";
|
|
2170
|
+
requiresSudo = true;
|
|
1739
2171
|
binary = "flatpak";
|
|
1740
2172
|
async checkUpdates(options) {
|
|
1741
2173
|
const startTime = Date.now();
|
|
@@ -1752,23 +2184,43 @@ var FlatpakManager = class extends BasePackageManager {
|
|
|
1752
2184
|
};
|
|
1753
2185
|
}
|
|
1754
2186
|
try {
|
|
1755
|
-
const
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
const
|
|
1762
|
-
|
|
1763
|
-
const parts = trimmed.split(/\s+/);
|
|
1764
|
-
updates.push({
|
|
2187
|
+
const timeoutMs = options?.timeoutMs ?? 2e4;
|
|
2188
|
+
const [userRes, systemRes] = await Promise.all([
|
|
2189
|
+
safeExec("flatpak", ["remote-ls", "--user", "--updates"], { timeoutMs }),
|
|
2190
|
+
safeExec("flatpak", ["remote-ls", "--system", "--updates"], { timeoutMs })
|
|
2191
|
+
]);
|
|
2192
|
+
if (!userRes.success && !systemRes.success) {
|
|
2193
|
+
const raw = (systemRes.stderr || userRes.stderr).trim();
|
|
2194
|
+
return {
|
|
1765
2195
|
managerId: this.id,
|
|
1766
2196
|
managerName: this.name,
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
2197
|
+
icon: this.icon,
|
|
2198
|
+
category: this.category,
|
|
2199
|
+
available: true,
|
|
2200
|
+
updates: [],
|
|
2201
|
+
durationMs: Date.now() - startTime,
|
|
2202
|
+
error: raw ? extractErrorMessage(raw) : void 0
|
|
2203
|
+
};
|
|
2204
|
+
}
|
|
2205
|
+
const updates = [];
|
|
2206
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2207
|
+
for (const execRes of [userRes, systemRes]) {
|
|
2208
|
+
for (const line of execRes.stdout.split("\n")) {
|
|
2209
|
+
const trimmed = line.trim();
|
|
2210
|
+
if (!trimmed) continue;
|
|
2211
|
+
const parts = trimmed.split(/\s+/);
|
|
2212
|
+
const name = parts[0];
|
|
2213
|
+
if (seen.has(name)) continue;
|
|
2214
|
+
seen.add(name);
|
|
2215
|
+
updates.push({
|
|
2216
|
+
managerId: this.id,
|
|
2217
|
+
managerName: this.name,
|
|
2218
|
+
name,
|
|
2219
|
+
currentVersion: "installed",
|
|
2220
|
+
latestVersion: parts[1] ?? "latest",
|
|
2221
|
+
type: "flatpak"
|
|
2222
|
+
});
|
|
2223
|
+
}
|
|
1772
2224
|
}
|
|
1773
2225
|
return {
|
|
1774
2226
|
managerId: this.id,
|
|
@@ -1792,17 +2244,56 @@ var FlatpakManager = class extends BasePackageManager {
|
|
|
1792
2244
|
};
|
|
1793
2245
|
}
|
|
1794
2246
|
}
|
|
2247
|
+
async hasSystemScopeApps() {
|
|
2248
|
+
const result = await safeExec("flatpak", ["list", "--system", "--columns=application"], {
|
|
2249
|
+
timeoutMs: 1e4
|
|
2250
|
+
});
|
|
2251
|
+
return result.success && result.stdout.trim().length > 0;
|
|
2252
|
+
}
|
|
1795
2253
|
async executeUpdate(items, options) {
|
|
1796
2254
|
const startTime = Date.now();
|
|
1797
2255
|
const steps = [];
|
|
1798
|
-
const
|
|
1799
|
-
"flatpak update
|
|
2256
|
+
const userStep = await this.executeStep(
|
|
2257
|
+
"flatpak update --user",
|
|
1800
2258
|
"flatpak",
|
|
1801
|
-
["update", "-y", "--noninteractive"],
|
|
2259
|
+
["update", "--user", "-y", "--noninteractive"],
|
|
1802
2260
|
options
|
|
1803
2261
|
);
|
|
1804
|
-
steps.push(
|
|
1805
|
-
const
|
|
2262
|
+
steps.push(userStep);
|
|
2263
|
+
const sudoStepName = "sudo flatpak update --system";
|
|
2264
|
+
const sudoStepCommand = "sudo flatpak update --system -y --noninteractive";
|
|
2265
|
+
if (options.dryRun) {
|
|
2266
|
+
const systemStep = await this.executeStep(
|
|
2267
|
+
sudoStepName,
|
|
2268
|
+
"sudo",
|
|
2269
|
+
["flatpak", "update", "--system", "-y", "--noninteractive"],
|
|
2270
|
+
options
|
|
2271
|
+
);
|
|
2272
|
+
steps.push(systemStep);
|
|
2273
|
+
} else if (process.getuid?.() === 0 || await this.hasPasswordlessSudo()) {
|
|
2274
|
+
const systemStep = await this.executeStep(
|
|
2275
|
+
sudoStepName,
|
|
2276
|
+
"sudo",
|
|
2277
|
+
["flatpak", "update", "--system", "-y", "--noninteractive"],
|
|
2278
|
+
options
|
|
2279
|
+
);
|
|
2280
|
+
steps.push(systemStep);
|
|
2281
|
+
} else if (await this.hasSystemScopeApps()) {
|
|
2282
|
+
steps.push({
|
|
2283
|
+
name: sudoStepName,
|
|
2284
|
+
command: sudoStepCommand,
|
|
2285
|
+
status: "failed",
|
|
2286
|
+
error: `System-wide Flatpak updates require sudo privileges. ${SUDO_REQUIRED_HINT}`
|
|
2287
|
+
});
|
|
2288
|
+
} else {
|
|
2289
|
+
steps.push({
|
|
2290
|
+
name: sudoStepName,
|
|
2291
|
+
command: sudoStepCommand,
|
|
2292
|
+
status: "skipped"
|
|
2293
|
+
});
|
|
2294
|
+
}
|
|
2295
|
+
const executedSteps = steps.filter((s) => s.status !== "skipped");
|
|
2296
|
+
const success = executedSteps.length > 0 && executedSteps.every((s) => s.status === "success");
|
|
1806
2297
|
return {
|
|
1807
2298
|
managerId: this.id,
|
|
1808
2299
|
managerName: this.name,
|
|
@@ -1820,6 +2311,7 @@ var DnfManager = class extends BasePackageManager {
|
|
|
1820
2311
|
name = "DNF (Fedora/RHEL)";
|
|
1821
2312
|
icon = "\u{1F4E6}";
|
|
1822
2313
|
category = "system";
|
|
2314
|
+
requiresSudo = true;
|
|
1823
2315
|
binary = "dnf";
|
|
1824
2316
|
async checkUpdates(options) {
|
|
1825
2317
|
const startTime = Date.now();
|
|
@@ -1917,8 +2409,8 @@ var DnfManager = class extends BasePackageManager {
|
|
|
1917
2409
|
const startTime = Date.now();
|
|
1918
2410
|
const steps = [];
|
|
1919
2411
|
if (!options.dryRun && process.getuid?.() !== 0) {
|
|
1920
|
-
|
|
1921
|
-
|
|
2412
|
+
if (!await this.hasPasswordlessSudo()) {
|
|
2413
|
+
const error = `DNF requires sudo privileges. ${SUDO_REQUIRED_HINT}`;
|
|
1922
2414
|
return {
|
|
1923
2415
|
managerId: this.id,
|
|
1924
2416
|
managerName: this.name,
|
|
@@ -1931,10 +2423,10 @@ var DnfManager = class extends BasePackageManager {
|
|
|
1931
2423
|
name: "sudo privileges",
|
|
1932
2424
|
command: "sudo -n true",
|
|
1933
2425
|
status: "failed",
|
|
1934
|
-
error
|
|
2426
|
+
error
|
|
1935
2427
|
}
|
|
1936
2428
|
],
|
|
1937
|
-
error
|
|
2429
|
+
error
|
|
1938
2430
|
};
|
|
1939
2431
|
}
|
|
1940
2432
|
}
|
|
@@ -2002,6 +2494,170 @@ var DnfManager = class extends BasePackageManager {
|
|
|
2002
2494
|
}
|
|
2003
2495
|
};
|
|
2004
2496
|
|
|
2497
|
+
// src/managers/firmware.ts
|
|
2498
|
+
var FirmwareManager = class extends BasePackageManager {
|
|
2499
|
+
id = "firmware";
|
|
2500
|
+
name = "Firmware (fwupd)";
|
|
2501
|
+
icon = "\u{1F527}";
|
|
2502
|
+
category = "system";
|
|
2503
|
+
requiresSudo = true;
|
|
2504
|
+
binary = "fwupdmgr";
|
|
2505
|
+
async isAvailable() {
|
|
2506
|
+
if (process.platform !== "linux") return false;
|
|
2507
|
+
return commandExists("fwupdmgr");
|
|
2508
|
+
}
|
|
2509
|
+
parseUpdates(stdout) {
|
|
2510
|
+
const jsonStart = stdout.indexOf("{");
|
|
2511
|
+
const jsonEnd = stdout.lastIndexOf("}");
|
|
2512
|
+
if (jsonStart === -1 || jsonEnd === -1 || jsonEnd <= jsonStart) return [];
|
|
2513
|
+
let parsed;
|
|
2514
|
+
try {
|
|
2515
|
+
parsed = JSON.parse(stdout.slice(jsonStart, jsonEnd + 1));
|
|
2516
|
+
} catch {
|
|
2517
|
+
return [];
|
|
2518
|
+
}
|
|
2519
|
+
const updates = [];
|
|
2520
|
+
for (const device of parsed.Devices ?? []) {
|
|
2521
|
+
const latestVersion = device.Releases?.[0]?.Version;
|
|
2522
|
+
if (!device.Name || !latestVersion) continue;
|
|
2523
|
+
updates.push({
|
|
2524
|
+
managerId: this.id,
|
|
2525
|
+
managerName: this.name,
|
|
2526
|
+
name: device.Name,
|
|
2527
|
+
currentVersion: device.Version ?? "unknown",
|
|
2528
|
+
latestVersion,
|
|
2529
|
+
type: "firmware",
|
|
2530
|
+
extra: device.Vendor ?? device.Plugin
|
|
2531
|
+
});
|
|
2532
|
+
}
|
|
2533
|
+
return updates;
|
|
2534
|
+
}
|
|
2535
|
+
async checkUpdates(options) {
|
|
2536
|
+
const startTime = Date.now();
|
|
2537
|
+
const isAvail = await this.isAvailable();
|
|
2538
|
+
if (!isAvail) {
|
|
2539
|
+
return {
|
|
2540
|
+
managerId: this.id,
|
|
2541
|
+
managerName: this.name,
|
|
2542
|
+
icon: this.icon,
|
|
2543
|
+
category: this.category,
|
|
2544
|
+
available: false,
|
|
2545
|
+
updates: [],
|
|
2546
|
+
durationMs: 0
|
|
2547
|
+
};
|
|
2548
|
+
}
|
|
2549
|
+
try {
|
|
2550
|
+
const timeoutMs = options?.timeoutMs ?? 3e4;
|
|
2551
|
+
await safeExec("fwupdmgr", ["refresh"], { timeoutMs: Math.min(timeoutMs, 2e4) });
|
|
2552
|
+
const execRes = await safeExec("fwupdmgr", ["--json", "get-updates"], { timeoutMs });
|
|
2553
|
+
const updates = this.parseUpdates(execRes.stdout);
|
|
2554
|
+
const parsedOutput = execRes.stdout.trim().startsWith("{");
|
|
2555
|
+
if (!execRes.success && !parsedOutput) {
|
|
2556
|
+
const raw = (execRes.stderr || execRes.stdout).trim();
|
|
2557
|
+
return {
|
|
2558
|
+
managerId: this.id,
|
|
2559
|
+
managerName: this.name,
|
|
2560
|
+
icon: this.icon,
|
|
2561
|
+
category: this.category,
|
|
2562
|
+
available: true,
|
|
2563
|
+
updates: [],
|
|
2564
|
+
durationMs: Date.now() - startTime,
|
|
2565
|
+
error: raw ? extractErrorMessage(raw) : "fwupdmgr get-updates failed"
|
|
2566
|
+
};
|
|
2567
|
+
}
|
|
2568
|
+
return {
|
|
2569
|
+
managerId: this.id,
|
|
2570
|
+
managerName: this.name,
|
|
2571
|
+
icon: this.icon,
|
|
2572
|
+
category: this.category,
|
|
2573
|
+
available: true,
|
|
2574
|
+
updates,
|
|
2575
|
+
durationMs: Date.now() - startTime
|
|
2576
|
+
};
|
|
2577
|
+
} catch (err) {
|
|
2578
|
+
return {
|
|
2579
|
+
managerId: this.id,
|
|
2580
|
+
managerName: this.name,
|
|
2581
|
+
icon: this.icon,
|
|
2582
|
+
category: this.category,
|
|
2583
|
+
available: true,
|
|
2584
|
+
updates: [],
|
|
2585
|
+
durationMs: Date.now() - startTime,
|
|
2586
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2587
|
+
};
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
async executeUpdate(items, options) {
|
|
2591
|
+
const startTime = Date.now();
|
|
2592
|
+
const steps = [];
|
|
2593
|
+
if (items.length === 0) {
|
|
2594
|
+
return {
|
|
2595
|
+
managerId: this.id,
|
|
2596
|
+
managerName: this.name,
|
|
2597
|
+
icon: this.icon,
|
|
2598
|
+
success: true,
|
|
2599
|
+
updatedCount: 0,
|
|
2600
|
+
durationMs: Date.now() - startTime,
|
|
2601
|
+
steps
|
|
2602
|
+
};
|
|
2603
|
+
}
|
|
2604
|
+
if (!options.dryRun && process.getuid?.() !== 0) {
|
|
2605
|
+
if (!await this.hasPasswordlessSudo()) {
|
|
2606
|
+
const error = `Firmware updates require sudo privileges. ${SUDO_REQUIRED_HINT}`;
|
|
2607
|
+
return {
|
|
2608
|
+
managerId: this.id,
|
|
2609
|
+
managerName: this.name,
|
|
2610
|
+
icon: this.icon,
|
|
2611
|
+
success: false,
|
|
2612
|
+
updatedCount: 0,
|
|
2613
|
+
durationMs: Date.now() - startTime,
|
|
2614
|
+
steps: [
|
|
2615
|
+
{
|
|
2616
|
+
name: "sudo privileges",
|
|
2617
|
+
command: "sudo -n true",
|
|
2618
|
+
status: "failed",
|
|
2619
|
+
error
|
|
2620
|
+
}
|
|
2621
|
+
],
|
|
2622
|
+
error
|
|
2623
|
+
};
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
const refreshStep = await this.executeStep(
|
|
2627
|
+
"sudo fwupdmgr refresh",
|
|
2628
|
+
"sudo",
|
|
2629
|
+
["fwupdmgr", "refresh", "--force"],
|
|
2630
|
+
options
|
|
2631
|
+
);
|
|
2632
|
+
steps.push(refreshStep);
|
|
2633
|
+
const updateStep = await this.executeStep(
|
|
2634
|
+
"sudo fwupdmgr update",
|
|
2635
|
+
"sudo",
|
|
2636
|
+
[
|
|
2637
|
+
"fwupdmgr",
|
|
2638
|
+
"update",
|
|
2639
|
+
"-y",
|
|
2640
|
+
"--no-reboot-check",
|
|
2641
|
+
"--no-unreported-check",
|
|
2642
|
+
"--no-metadata-check"
|
|
2643
|
+
],
|
|
2644
|
+
options
|
|
2645
|
+
);
|
|
2646
|
+
steps.push(updateStep);
|
|
2647
|
+
const success = steps.every((s) => s.status === "success");
|
|
2648
|
+
return {
|
|
2649
|
+
managerId: this.id,
|
|
2650
|
+
managerName: this.name,
|
|
2651
|
+
icon: this.icon,
|
|
2652
|
+
success,
|
|
2653
|
+
updatedCount: success ? items.length : 0,
|
|
2654
|
+
durationMs: Date.now() - startTime,
|
|
2655
|
+
steps,
|
|
2656
|
+
error: !success ? steps.find((s) => s.status === "failed")?.error : void 0
|
|
2657
|
+
};
|
|
2658
|
+
}
|
|
2659
|
+
};
|
|
2660
|
+
|
|
2005
2661
|
// src/managers/registry.ts
|
|
2006
2662
|
function createDefaultManagers() {
|
|
2007
2663
|
return [
|
|
@@ -2012,13 +2668,17 @@ function createDefaultManagers() {
|
|
|
2012
2668
|
new YarnManager(),
|
|
2013
2669
|
new PythonPipManager(),
|
|
2014
2670
|
new PipxManager(),
|
|
2671
|
+
new UvManager(),
|
|
2672
|
+
new PoetryManager(),
|
|
2673
|
+
new CondaManager(),
|
|
2015
2674
|
new CargoManager(),
|
|
2016
2675
|
new RubyGemManager(),
|
|
2017
2676
|
new MacPortsManager(),
|
|
2018
2677
|
new MacAppStoreManager(),
|
|
2019
2678
|
new AptManager(),
|
|
2020
2679
|
new DnfManager(),
|
|
2021
|
-
new FlatpakManager()
|
|
2680
|
+
new FlatpakManager(),
|
|
2681
|
+
new FirmwareManager()
|
|
2022
2682
|
];
|
|
2023
2683
|
}
|
|
2024
2684
|
function filterManagers(managers, only, exclude) {
|
|
@@ -2154,10 +2814,260 @@ var UpdaterEngine = class {
|
|
|
2154
2814
|
}
|
|
2155
2815
|
};
|
|
2156
2816
|
|
|
2817
|
+
// src/core/distro.ts
|
|
2818
|
+
import { readFileSync } from "fs";
|
|
2819
|
+
var FEDORA_RELEASES_URL = "https://bodhi.fedoraproject.org/releases?state=current";
|
|
2820
|
+
var UBUNTU_META_RELEASE_URL = "https://changelogs.ubuntu.com/meta-release";
|
|
2821
|
+
var DEBIAN_STABLE_RELEASE_URL = "https://deb.debian.org/debian/dists/stable/Release";
|
|
2822
|
+
function parseOsRelease(content) {
|
|
2823
|
+
const values = {};
|
|
2824
|
+
for (const rawLine of content.split("\n")) {
|
|
2825
|
+
const line = rawLine.trim();
|
|
2826
|
+
if (!line || line.startsWith("#")) continue;
|
|
2827
|
+
const match = line.match(/^([A-Z0-9_]+)=(.*)$/);
|
|
2828
|
+
if (!match) continue;
|
|
2829
|
+
values[match[1]] = match[2].replace(/^"(.*)"$/, "$1");
|
|
2830
|
+
}
|
|
2831
|
+
const id = values.ID?.toLowerCase();
|
|
2832
|
+
if (!id) return null;
|
|
2833
|
+
return {
|
|
2834
|
+
id,
|
|
2835
|
+
idLike: (values.ID_LIKE ?? "").toLowerCase().split(/\s+/).filter(Boolean),
|
|
2836
|
+
versionId: values.VERSION_ID ?? "",
|
|
2837
|
+
codename: values.VERSION_CODENAME ?? "",
|
|
2838
|
+
prettyName: values.PRETTY_NAME ?? values.NAME ?? id
|
|
2839
|
+
};
|
|
2840
|
+
}
|
|
2841
|
+
function readDistroIdentity() {
|
|
2842
|
+
try {
|
|
2843
|
+
return parseOsRelease(readFileSync("/etc/os-release", "utf8"));
|
|
2844
|
+
} catch {
|
|
2845
|
+
return null;
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
function numericVersion(value) {
|
|
2849
|
+
const parsed = Number.parseFloat(value);
|
|
2850
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
2851
|
+
}
|
|
2852
|
+
async function fetchText(url, timeoutMs) {
|
|
2853
|
+
try {
|
|
2854
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
2855
|
+
if (!response.ok) return void 0;
|
|
2856
|
+
return await response.text();
|
|
2857
|
+
} catch {
|
|
2858
|
+
return void 0;
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
async function fetchJson(url, timeoutMs) {
|
|
2862
|
+
try {
|
|
2863
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
2864
|
+
if (!response.ok) return void 0;
|
|
2865
|
+
return await response.json();
|
|
2866
|
+
} catch {
|
|
2867
|
+
return void 0;
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
async function detectFedora(identity, timeoutMs) {
|
|
2871
|
+
const current = numericVersion(identity.versionId);
|
|
2872
|
+
if (current === void 0) return null;
|
|
2873
|
+
const data = await fetchJson(FEDORA_RELEASES_URL, timeoutMs);
|
|
2874
|
+
const versions = (data?.releases ?? []).filter((release) => release.id_prefix === "FEDORA" && release.version !== void 0).map((release) => numericVersion(release.version ?? "")).filter((version) => version !== void 0 && Number.isInteger(version));
|
|
2875
|
+
if (versions.length === 0) return null;
|
|
2876
|
+
const latest = Math.max(...versions);
|
|
2877
|
+
if (latest <= current) return null;
|
|
2878
|
+
return {
|
|
2879
|
+
family: "fedora",
|
|
2880
|
+
currentVersion: identity.versionId,
|
|
2881
|
+
nextVersion: String(latest),
|
|
2882
|
+
targetName: `Fedora Linux ${latest}`,
|
|
2883
|
+
supported: true,
|
|
2884
|
+
steps: [
|
|
2885
|
+
{
|
|
2886
|
+
name: `sudo dnf system-upgrade download --releasever=${latest}`,
|
|
2887
|
+
file: "sudo",
|
|
2888
|
+
args: ["dnf", "system-upgrade", "download", `--releasever=${latest}`, "-y"],
|
|
2889
|
+
requiresSudo: true
|
|
2890
|
+
}
|
|
2891
|
+
],
|
|
2892
|
+
rebootRequired: true,
|
|
2893
|
+
rebootCommand: "sudo dnf system-upgrade reboot",
|
|
2894
|
+
guidance: [
|
|
2895
|
+
`Packages for Fedora ${latest} are downloaded and staged; the upgrade runs offline during the next boot.`,
|
|
2896
|
+
`Reboot to apply it: sudo dnf system-upgrade reboot`,
|
|
2897
|
+
"If the system-upgrade plugin is missing, install `dnf-plugin-system-upgrade` (dnf4) or `dnf5-plugin-system-upgrade` (dnf5)."
|
|
2898
|
+
]
|
|
2899
|
+
};
|
|
2900
|
+
}
|
|
2901
|
+
function parseUbuntuMetaRelease(content) {
|
|
2902
|
+
const blocks = [];
|
|
2903
|
+
for (const chunk of content.split(/\n\s*\n/)) {
|
|
2904
|
+
const fields = {};
|
|
2905
|
+
for (const line of chunk.split("\n")) {
|
|
2906
|
+
const match = line.match(/^([A-Za-z-]+):\s*(.*)$/);
|
|
2907
|
+
if (match) fields[match[1].toLowerCase()] = match[2].trim();
|
|
2908
|
+
}
|
|
2909
|
+
if (fields.dist && fields.version) {
|
|
2910
|
+
blocks.push({
|
|
2911
|
+
dist: fields.dist,
|
|
2912
|
+
version: fields.version,
|
|
2913
|
+
supported: fields.supported === "1"
|
|
2914
|
+
});
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
return blocks;
|
|
2918
|
+
}
|
|
2919
|
+
async function detectUbuntu(identity, timeoutMs) {
|
|
2920
|
+
const current = numericVersion(identity.versionId);
|
|
2921
|
+
if (current === void 0) return null;
|
|
2922
|
+
if (await commandExists("do-release-upgrade")) {
|
|
2923
|
+
const check = await safeExec("do-release-upgrade", ["-c"], {
|
|
2924
|
+
timeoutMs: Math.max(timeoutMs, 2e4)
|
|
2925
|
+
});
|
|
2926
|
+
const output = `${check.stdout}
|
|
2927
|
+
${check.stderr}`;
|
|
2928
|
+
const match = output.match(/New release '([^']+)' available/i);
|
|
2929
|
+
if (match) {
|
|
2930
|
+
return {
|
|
2931
|
+
family: "ubuntu",
|
|
2932
|
+
currentVersion: identity.versionId,
|
|
2933
|
+
nextVersion: match[1],
|
|
2934
|
+
targetName: `Ubuntu ${match[1]}`,
|
|
2935
|
+
supported: true,
|
|
2936
|
+
steps: [
|
|
2937
|
+
{
|
|
2938
|
+
name: "sudo do-release-upgrade -f DistUpgradeViewNonInteractive",
|
|
2939
|
+
file: "sudo",
|
|
2940
|
+
args: ["do-release-upgrade", "-f", "DistUpgradeViewNonInteractive"],
|
|
2941
|
+
requiresSudo: true
|
|
2942
|
+
}
|
|
2943
|
+
],
|
|
2944
|
+
rebootRequired: true,
|
|
2945
|
+
rebootCommand: "sudo reboot",
|
|
2946
|
+
guidance: [
|
|
2947
|
+
"The release upgrade runs non-interactively and may take a long time; reboot when it asks or once it reports completion.",
|
|
2948
|
+
"Make sure your data is backed up before upgrading the distribution."
|
|
2949
|
+
]
|
|
2950
|
+
};
|
|
2951
|
+
}
|
|
2952
|
+
if (/no new release found/i.test(output)) return null;
|
|
2953
|
+
}
|
|
2954
|
+
const meta = await fetchText(UBUNTU_META_RELEASE_URL, timeoutMs);
|
|
2955
|
+
if (!meta) return null;
|
|
2956
|
+
const candidates = parseUbuntuMetaRelease(meta).filter((release) => release.supported).map((release) => ({ ...release, numeric: numericVersion(release.version) })).filter(
|
|
2957
|
+
(release) => release.numeric !== void 0 && release.numeric > current
|
|
2958
|
+
).sort((a, b) => a.numeric - b.numeric);
|
|
2959
|
+
const next = candidates[0];
|
|
2960
|
+
if (!next) return null;
|
|
2961
|
+
return {
|
|
2962
|
+
family: "ubuntu",
|
|
2963
|
+
currentVersion: identity.versionId,
|
|
2964
|
+
nextVersion: next.version,
|
|
2965
|
+
targetName: `Ubuntu ${next.version}`,
|
|
2966
|
+
supported: false,
|
|
2967
|
+
steps: [],
|
|
2968
|
+
rebootRequired: true,
|
|
2969
|
+
guidance: [
|
|
2970
|
+
`Ubuntu ${next.version} is available, but the official upgrader is not installed.`,
|
|
2971
|
+
"Install it with: sudo apt install update-manager-core",
|
|
2972
|
+
"Then run: sudo do-release-upgrade"
|
|
2973
|
+
]
|
|
2974
|
+
};
|
|
2975
|
+
}
|
|
2976
|
+
async function detectDebian(identity, timeoutMs) {
|
|
2977
|
+
const current = numericVersion(identity.versionId);
|
|
2978
|
+
if (current === void 0) return null;
|
|
2979
|
+
const release = await fetchText(DEBIAN_STABLE_RELEASE_URL, timeoutMs);
|
|
2980
|
+
if (!release) return null;
|
|
2981
|
+
const versionMatch = release.match(/^Version:\s*(\S+)$/m);
|
|
2982
|
+
const codenameMatch = release.match(/^Codename:\s*(\S+)$/m);
|
|
2983
|
+
if (!versionMatch) return null;
|
|
2984
|
+
const stable = numericVersion(versionMatch[1]);
|
|
2985
|
+
if (stable === void 0) return null;
|
|
2986
|
+
const stableMajor = Math.trunc(stable);
|
|
2987
|
+
if (stableMajor <= Math.trunc(current)) return null;
|
|
2988
|
+
const codename = codenameMatch?.[1] ?? `debian-${stableMajor}`;
|
|
2989
|
+
const currentCodename = identity.codename || `version ${identity.versionId}`;
|
|
2990
|
+
return {
|
|
2991
|
+
family: "debian",
|
|
2992
|
+
currentVersion: identity.versionId,
|
|
2993
|
+
nextVersion: String(stableMajor),
|
|
2994
|
+
targetName: `Debian ${stableMajor} (${codename})`,
|
|
2995
|
+
// Debian has no supported unattended in-place upgrader: sources.list must switch
|
|
2996
|
+
// suites deliberately, so Heimdall only reports the procedure.
|
|
2997
|
+
supported: false,
|
|
2998
|
+
steps: [],
|
|
2999
|
+
rebootRequired: true,
|
|
3000
|
+
guidance: [
|
|
3001
|
+
`Debian ${stableMajor} (${codename}) is available; this system runs ${identity.versionId} (${currentCodename}).`,
|
|
3002
|
+
`Replace "${currentCodename}" with "${codename}" in /etc/apt/sources.list and /etc/apt/sources.list.d/*.list.`,
|
|
3003
|
+
"Then run: sudo apt update && sudo apt full-upgrade",
|
|
3004
|
+
"Back up your data first and expect to reboot afterwards. Debian does not offer an unattended upgrade path."
|
|
3005
|
+
]
|
|
3006
|
+
};
|
|
3007
|
+
}
|
|
3008
|
+
async function detectDistroUpgrade(options) {
|
|
3009
|
+
if (process.platform !== "linux") return null;
|
|
3010
|
+
const identity = options?.identity ?? readDistroIdentity();
|
|
3011
|
+
if (!identity) return null;
|
|
3012
|
+
const timeoutMs = options?.timeoutMs ?? 6e3;
|
|
3013
|
+
const family = identity.id === "fedora" ? "fedora" : identity.id === "ubuntu" || identity.idLike.includes("ubuntu") ? "ubuntu" : identity.id === "debian" ? "debian" : void 0;
|
|
3014
|
+
try {
|
|
3015
|
+
if (family === "fedora") return await detectFedora(identity, timeoutMs);
|
|
3016
|
+
if (family === "ubuntu") return await detectUbuntu(identity, timeoutMs);
|
|
3017
|
+
if (family === "debian") return await detectDebian(identity, timeoutMs);
|
|
3018
|
+
return null;
|
|
3019
|
+
} catch {
|
|
3020
|
+
return null;
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
async function applyDistroUpgrade(plan, options) {
|
|
3024
|
+
const steps = [];
|
|
3025
|
+
if (!plan.supported || plan.steps.length === 0) {
|
|
3026
|
+
return {
|
|
3027
|
+
success: false,
|
|
3028
|
+
steps,
|
|
3029
|
+
error: `No unattended upgrade routine is available for ${plan.targetName}.`
|
|
3030
|
+
};
|
|
3031
|
+
}
|
|
3032
|
+
if (!options.dryRun && process.getuid?.() !== 0) {
|
|
3033
|
+
const ticket = await safeExec("sudo", ["-n", "true"], { timeoutMs: 3e3 });
|
|
3034
|
+
if (!ticket.success) {
|
|
3035
|
+
const error = `The distribution upgrade requires sudo privileges. Run "sudo -v" in this terminal and retry.`;
|
|
3036
|
+
return {
|
|
3037
|
+
success: false,
|
|
3038
|
+
steps: [
|
|
3039
|
+
{
|
|
3040
|
+
name: "sudo privileges",
|
|
3041
|
+
command: "sudo -n true",
|
|
3042
|
+
status: "failed",
|
|
3043
|
+
error
|
|
3044
|
+
}
|
|
3045
|
+
],
|
|
3046
|
+
error
|
|
3047
|
+
};
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
for (const step of plan.steps) {
|
|
3051
|
+
const result = await runStep(step.name, step.file, step.args, {
|
|
3052
|
+
...options,
|
|
3053
|
+
// Distribution upgrades are long operations: allow up to one hour per step.
|
|
3054
|
+
timeoutMs: 36e5
|
|
3055
|
+
});
|
|
3056
|
+
steps.push(result);
|
|
3057
|
+
if (result.status !== "success") break;
|
|
3058
|
+
}
|
|
3059
|
+
const success = steps.length > 0 && steps.every((step) => step.status === "success");
|
|
3060
|
+
return {
|
|
3061
|
+
success,
|
|
3062
|
+
steps,
|
|
3063
|
+
error: !success ? steps.find((step) => step.status === "failed")?.error : void 0
|
|
3064
|
+
};
|
|
3065
|
+
}
|
|
3066
|
+
|
|
2157
3067
|
// src/utils/version.ts
|
|
2158
3068
|
import { fileURLToPath } from "url";
|
|
2159
3069
|
import { dirname, resolve } from "path";
|
|
2160
|
-
import { existsSync, readFileSync } from "fs";
|
|
3070
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
2161
3071
|
var FALLBACK_METADATA = {
|
|
2162
3072
|
name: "@dahch/heimdall",
|
|
2163
3073
|
version: "1.0.1",
|
|
@@ -2167,7 +3077,7 @@ var cachedMetadata = null;
|
|
|
2167
3077
|
var customFsAdapter = null;
|
|
2168
3078
|
function resolvePackageMetadata(options) {
|
|
2169
3079
|
const exists = options?.fsAdapter?.existsSync ?? customFsAdapter?.existsSync ?? existsSync;
|
|
2170
|
-
const read = options?.fsAdapter?.readFileSync ?? customFsAdapter?.readFileSync ??
|
|
3080
|
+
const read = options?.fsAdapter?.readFileSync ?? customFsAdapter?.readFileSync ?? readFileSync2;
|
|
2171
3081
|
try {
|
|
2172
3082
|
let currentDir;
|
|
2173
3083
|
if (options?.basePath) {
|
|
@@ -2220,10 +3130,127 @@ function getPackageMetadata(options) {
|
|
|
2220
3130
|
}
|
|
2221
3131
|
|
|
2222
3132
|
// src/cli.ts
|
|
3133
|
+
async function offerDistroUpgrade(options, timeoutMs, isSimulated) {
|
|
3134
|
+
if (process.platform !== "linux" || options.distroUpgrade === false) return true;
|
|
3135
|
+
const scanSpinner = p.spinner();
|
|
3136
|
+
scanSpinner.start("Checking for distribution release upgrades...");
|
|
3137
|
+
let plan = null;
|
|
3138
|
+
try {
|
|
3139
|
+
plan = await detectDistroUpgrade({ timeoutMs: Math.max(timeoutMs, 6e3) });
|
|
3140
|
+
} catch {
|
|
3141
|
+
plan = null;
|
|
3142
|
+
}
|
|
3143
|
+
scanSpinner.stop(
|
|
3144
|
+
plan ? `Distribution upgrade available: ${plan.targetName}` : "Distribution release is up to date"
|
|
3145
|
+
);
|
|
3146
|
+
if (!plan) {
|
|
3147
|
+
if (options.verbose) {
|
|
3148
|
+
p.log.step(pc2.dim("No newer distribution release detected."));
|
|
3149
|
+
}
|
|
3150
|
+
return true;
|
|
3151
|
+
}
|
|
3152
|
+
p.note(
|
|
3153
|
+
[
|
|
3154
|
+
`${pc2.bold(plan.targetName)} is available ${pc2.dim(`(current: ${plan.currentVersion})`)}.`,
|
|
3155
|
+
plan.supported ? "Heimdall can stage/apply the upgrade; it never reboots the machine by itself." : "Heimdall cannot apply this upgrade automatically."
|
|
3156
|
+
].join("\n"),
|
|
3157
|
+
"Distribution upgrade available"
|
|
3158
|
+
);
|
|
3159
|
+
for (const line of plan.guidance) {
|
|
3160
|
+
p.log.step(pc2.dim(`\u21B3 ${line}`));
|
|
3161
|
+
}
|
|
3162
|
+
if (isSimulated) {
|
|
3163
|
+
for (const step of plan.steps) {
|
|
3164
|
+
p.log.info(pc2.yellow(`[DRY RUN] Would run: ${[step.file, ...step.args].join(" ")}`));
|
|
3165
|
+
}
|
|
3166
|
+
return true;
|
|
3167
|
+
}
|
|
3168
|
+
if (!plan.supported) return true;
|
|
3169
|
+
const autoApply = options.upgradeDistro === true;
|
|
3170
|
+
const canPrompt = Boolean(process.stdin.isTTY) && !options.yes && !autoApply;
|
|
3171
|
+
if (!autoApply && !canPrompt) {
|
|
3172
|
+
const [first] = plan.steps;
|
|
3173
|
+
if (first) {
|
|
3174
|
+
p.log.info(
|
|
3175
|
+
pc2.dim(`Not applied automatically. Run: ${[first.file, ...first.args].join(" ")}`)
|
|
3176
|
+
);
|
|
3177
|
+
}
|
|
3178
|
+
return true;
|
|
3179
|
+
}
|
|
3180
|
+
let proceed = autoApply;
|
|
3181
|
+
if (!autoApply) {
|
|
3182
|
+
const answer = await p.confirm({
|
|
3183
|
+
message: `Apply the ${plan.targetName} release upgrade now?`,
|
|
3184
|
+
initialValue: false
|
|
3185
|
+
});
|
|
3186
|
+
if (p.isCancel(answer)) {
|
|
3187
|
+
p.cancel("Distribution upgrade cancelled.");
|
|
3188
|
+
return true;
|
|
3189
|
+
}
|
|
3190
|
+
proceed = answer;
|
|
3191
|
+
}
|
|
3192
|
+
if (!proceed) {
|
|
3193
|
+
p.log.info(pc2.yellow("Distribution upgrade skipped."));
|
|
3194
|
+
return true;
|
|
3195
|
+
}
|
|
3196
|
+
const upgradeSpinner = p.spinner();
|
|
3197
|
+
let stopSudoKeepAlive;
|
|
3198
|
+
if (process.getuid?.() !== 0) {
|
|
3199
|
+
p.log.step(pc2.dim("Requesting sudo privileges for the distribution upgrade..."));
|
|
3200
|
+
if (!await ensureSudo()) {
|
|
3201
|
+
p.log.warn(
|
|
3202
|
+
pc2.yellow(
|
|
3203
|
+
'Sudo privileges not granted: the distribution upgrade cannot run. Run "sudo -v" in this terminal and retry.'
|
|
3204
|
+
)
|
|
3205
|
+
);
|
|
3206
|
+
return false;
|
|
3207
|
+
}
|
|
3208
|
+
stopSudoKeepAlive = startSudoKeepAlive();
|
|
3209
|
+
}
|
|
3210
|
+
upgradeSpinner.start(pc2.dim("Running the distribution release upgrade..."));
|
|
3211
|
+
let result;
|
|
3212
|
+
try {
|
|
3213
|
+
result = await applyDistroUpgrade(plan, {
|
|
3214
|
+
onStepStart: (stepName) => upgradeSpinner.message(pc2.dim(`Running: ${stepName}...`)),
|
|
3215
|
+
onStepProgress: (_stepName, logLine) => {
|
|
3216
|
+
if (!logLine) return;
|
|
3217
|
+
const truncated = logLine.length > 55 ? `${logLine.slice(0, 52)}...` : logLine;
|
|
3218
|
+
upgradeSpinner.message(pc2.dim(truncated));
|
|
3219
|
+
},
|
|
3220
|
+
onStepEnd: (stepName, success, error, durationMs) => {
|
|
3221
|
+
const mark = success ? pc2.green("\u2714") : pc2.red("\u2716");
|
|
3222
|
+
const durStr = durationMs !== void 0 ? pc2.dim(` [${formatDuration(durationMs)}]`) : "";
|
|
3223
|
+
const detail = error ? pc2.red(` \u21B3 ${error.split("\n")[0]}`) : "";
|
|
3224
|
+
upgradeSpinner.stop(` ${mark} ${pc2.dim(stepName)}${durStr}${detail}`);
|
|
3225
|
+
upgradeSpinner.start(pc2.dim("Working..."));
|
|
3226
|
+
}
|
|
3227
|
+
});
|
|
3228
|
+
} finally {
|
|
3229
|
+
stopSudoKeepAlive?.();
|
|
3230
|
+
}
|
|
3231
|
+
upgradeSpinner.stop(
|
|
3232
|
+
result.success ? pc2.green("\u2714 Distribution upgrade routine completed") : pc2.red("\u2716 Distribution upgrade failed")
|
|
3233
|
+
);
|
|
3234
|
+
if (!result.success) {
|
|
3235
|
+
if (result.error) p.log.error(pc2.red(result.error));
|
|
3236
|
+
return false;
|
|
3237
|
+
}
|
|
3238
|
+
if (plan.rebootRequired) {
|
|
3239
|
+
p.note(
|
|
3240
|
+
`Reboot to finish the upgrade:
|
|
3241
|
+
${pc2.bold(plan.rebootCommand ?? "sudo reboot")}`,
|
|
3242
|
+
"Reboot required"
|
|
3243
|
+
);
|
|
3244
|
+
}
|
|
3245
|
+
return true;
|
|
3246
|
+
}
|
|
2223
3247
|
async function main() {
|
|
2224
3248
|
const { version, description } = getPackageMetadata();
|
|
2225
3249
|
const program = new Command();
|
|
2226
|
-
program.name("hmd").description(description || "Heimdall: resilient validator and updater for system and global packages").version(version).option("-y, --yes", "Skip confirmation prompt and update all automatically").option("-d, --dry-run", "Inspect outdated packages and simulate update without running changes").option("-o, --only <managers>", "Only check and update specified managers (comma-separated, e.g. brew,npm)").option("-x, --exclude <managers>", "Exclude specified managers (comma-separated, e.g. macports,pip)").option("-t, --timeout <ms>", "Timeout per check in milliseconds", "25000").option("-v, --verbose", "Show detailed output and logs").
|
|
3250
|
+
program.name("hmd").description(description || "Heimdall: resilient validator and updater for system and global packages").version(version).option("-y, --yes", "Skip confirmation prompt and update all automatically").option("-d, --dry-run", "Inspect outdated packages and simulate update without running changes").option("-o, --only <managers>", "Only check and update specified managers (comma-separated, e.g. brew,npm)").option("-x, --exclude <managers>", "Exclude specified managers (comma-separated, e.g. macports,pip)").option("-t, --timeout <ms>", "Timeout per check in milliseconds", "25000").option("-v, --verbose", "Show detailed output and logs").option("--no-distro-upgrade", "Skip the distribution release upgrade check at the end").option(
|
|
3251
|
+
"-U, --upgrade-distro",
|
|
3252
|
+
"Apply a detected distribution release upgrade without asking (never reboots)"
|
|
3253
|
+
).parse(process.argv);
|
|
2227
3254
|
const options = program.opts();
|
|
2228
3255
|
if (process.stdout.isTTY && !options.verbose) {
|
|
2229
3256
|
console.clear();
|
|
@@ -2269,8 +3296,9 @@ async function main() {
|
|
|
2269
3296
|
pc2.green("All detected package managers and global packages are completely up to date! \u{1F389}"),
|
|
2270
3297
|
"Status: Up to Date"
|
|
2271
3298
|
);
|
|
2272
|
-
|
|
2273
|
-
|
|
3299
|
+
const distroOk2 = await offerDistroUpgrade(options, timeoutMs, options.dryRun ?? false);
|
|
3300
|
+
p.outro(pc2.cyan("No package updates needed. Have a great day!"));
|
|
3301
|
+
process.exit(distroOk2 ? 0 : 1);
|
|
2274
3302
|
}
|
|
2275
3303
|
p.log.message(`
|
|
2276
3304
|
${pc2.bold("Pending updates")} ${pc2.dim(`(${allUpdates.length} total):`)}`);
|
|
@@ -2341,6 +3369,21 @@ ${pc2.bold("Pending updates")} ${pc2.dim(`(${allUpdates.length} total):`)}`);
|
|
|
2341
3369
|
process.exit(0);
|
|
2342
3370
|
}
|
|
2343
3371
|
const isSimulated = options.dryRun ?? false;
|
|
3372
|
+
let stopSudoKeepAlive;
|
|
3373
|
+
const selectedIds = new Set(managersToUpdate);
|
|
3374
|
+
const needsSudo = selectedManagers.some((m) => selectedIds.has(m.id) && m.requiresSudo);
|
|
3375
|
+
if (needsSudo && !isSimulated) {
|
|
3376
|
+
p.log.step(pc2.dim("Requesting sudo privileges required by system package managers..."));
|
|
3377
|
+
if (await ensureSudo()) {
|
|
3378
|
+
stopSudoKeepAlive = startSudoKeepAlive();
|
|
3379
|
+
} else {
|
|
3380
|
+
p.log.warn(
|
|
3381
|
+
pc2.yellow(
|
|
3382
|
+
'Sudo privileges not granted: system package managers will fail. Run "sudo -v" in this terminal and retry.'
|
|
3383
|
+
)
|
|
3384
|
+
);
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
2344
3387
|
p.log.step(
|
|
2345
3388
|
pc2.bold(
|
|
2346
3389
|
pc2.cyan(
|
|
@@ -2351,52 +3394,58 @@ ${isSimulated ? "[DRY RUN] Simulating" : "Starting"} updates for ${managersToUpd
|
|
|
2351
3394
|
);
|
|
2352
3395
|
const updateSpinner = p.spinner();
|
|
2353
3396
|
let managerIndex = 0;
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
3397
|
+
let summary;
|
|
3398
|
+
try {
|
|
3399
|
+
summary = await engine.execute(
|
|
3400
|
+
managersToUpdate,
|
|
3401
|
+
itemsByManager,
|
|
3402
|
+
{
|
|
3403
|
+
verbose: options.verbose,
|
|
3404
|
+
dryRun: isSimulated,
|
|
3405
|
+
confirmAction: async (message) => {
|
|
3406
|
+
if (options.yes) return true;
|
|
3407
|
+
updateSpinner.stop(pc2.dim("Waiting for user confirmation..."));
|
|
3408
|
+
const confirmed = await p.confirm({ message, initialValue: false });
|
|
3409
|
+
updateSpinner.start(pc2.dim("Continuing update..."));
|
|
3410
|
+
return typeof confirmed === "boolean" ? confirmed : false;
|
|
3411
|
+
},
|
|
3412
|
+
onStepStart: (stepName) => {
|
|
3413
|
+
updateSpinner.message(pc2.dim(`Running: ${stepName}...`));
|
|
3414
|
+
},
|
|
3415
|
+
onStepProgress: (stepName, logLine) => {
|
|
3416
|
+
if (logLine) {
|
|
3417
|
+
const truncated = logLine.length > 55 ? logLine.slice(0, 52) + "..." : logLine;
|
|
3418
|
+
updateSpinner.message(pc2.dim(`${stepName} [${truncated}]`));
|
|
3419
|
+
}
|
|
3420
|
+
},
|
|
3421
|
+
onStepEnd: (stepName, success, error, durationMs) => {
|
|
3422
|
+
const mark = success ? pc2.green("\u2714") : pc2.red("\u2716");
|
|
3423
|
+
const durStr = durationMs !== void 0 ? pc2.dim(` [${formatDuration(durationMs)}]`) : "";
|
|
3424
|
+
const detail = error ? pc2.red(` \u21B3 ${error.split("\n")[0]}`) : "";
|
|
3425
|
+
updateSpinner.stop(` ${mark} ${pc2.dim(stepName)}${durStr}${detail}`);
|
|
3426
|
+
updateSpinner.start(pc2.dim("Working..."));
|
|
2374
3427
|
}
|
|
2375
3428
|
},
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
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(`
|
|
3429
|
+
(mgr) => {
|
|
3430
|
+
managerIndex++;
|
|
3431
|
+
console.log(pc2.bold(pc2.cyan(`
|
|
2387
3432
|
\u25CF [${managerIndex}/${managersToUpdate.length}] ${mgr.icon} ${mgr.name}`)));
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
3433
|
+
updateSpinner.start(pc2.dim("Starting update routine..."));
|
|
3434
|
+
},
|
|
3435
|
+
(mgr, res) => {
|
|
3436
|
+
if (res.success) {
|
|
3437
|
+
updateSpinner.stop(pc2.green(` \u2714 ${mgr.name} completed in ${formatDuration(res.durationMs)}`));
|
|
3438
|
+
} else {
|
|
3439
|
+
updateSpinner.stop(pc2.red(` \u2716 ${mgr.name} completed with issues in ${formatDuration(res.durationMs)}`));
|
|
3440
|
+
}
|
|
2395
3441
|
}
|
|
2396
|
-
|
|
2397
|
-
|
|
3442
|
+
);
|
|
3443
|
+
} finally {
|
|
3444
|
+
stopSudoKeepAlive?.();
|
|
3445
|
+
}
|
|
2398
3446
|
console.log(renderExecutionSummary(summary.results));
|
|
2399
|
-
|
|
3447
|
+
const distroOk = await offerDistroUpgrade(options, timeoutMs, isSimulated);
|
|
3448
|
+
if (summary.failedCount === 0 && distroOk) {
|
|
2400
3449
|
p.outro(
|
|
2401
3450
|
pc2.bold(
|
|
2402
3451
|
pc2.green(
|
|
@@ -2406,10 +3455,11 @@ ${isSimulated ? "[DRY RUN] Simulating" : "Starting"} updates for ${managersToUpd
|
|
|
2406
3455
|
);
|
|
2407
3456
|
} else {
|
|
2408
3457
|
process.exitCode = 1;
|
|
3458
|
+
const distroNote = distroOk ? "" : ", distribution upgrade failed";
|
|
2409
3459
|
p.outro(
|
|
2410
3460
|
pc2.bold(
|
|
2411
3461
|
pc2.yellow(
|
|
2412
|
-
`Completed with warnings: ${summary.successCount} succeeded, ${summary.failedCount} failed (${formatDuration(summary.totalDurationMs)}).`
|
|
3462
|
+
`Completed with warnings: ${summary.successCount} succeeded, ${summary.failedCount} failed${distroNote} (${formatDuration(summary.totalDurationMs)}).`
|
|
2413
3463
|
)
|
|
2414
3464
|
)
|
|
2415
3465
|
);
|