@dahch/heimdall 1.2.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 +49 -3
- package/dist/cli.js +1257 -354
- package/package.json +14 -3
package/dist/cli.js
CHANGED
|
@@ -169,45 +169,48 @@ var BasePackageManager = class {
|
|
|
169
169
|
return result.success;
|
|
170
170
|
}
|
|
171
171
|
async executeStep(stepName, file, args, options, extraExecOptions) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
durationMs: 0
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
const result = await safeExec(file, args, {
|
|
185
|
-
...extraExecOptions,
|
|
186
|
-
timeoutMs: options.timeoutMs ?? 18e4,
|
|
187
|
-
// 3 min per step
|
|
188
|
-
onData: (data) => {
|
|
189
|
-
if (options.onStepProgress) {
|
|
190
|
-
const lines = data.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
191
|
-
if (lines.length > 0) {
|
|
192
|
-
options.onStepProgress(stepName, lines[lines.length - 1]);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
});
|
|
197
|
-
const durationMs = Date.now() - startTime;
|
|
198
|
-
const success = result.success;
|
|
199
|
-
const rawOutput = [result.stderr, result.stdout].filter(Boolean).join("\n").trim();
|
|
200
|
-
const errorMsg = !success ? result.timedOut ? `Step timed out after ${Math.round((options.timeoutMs ?? 18e4) / 1e3)}s` : extractErrorMessage(rawOutput) : void 0;
|
|
201
|
-
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);
|
|
202
181
|
return {
|
|
203
182
|
name: stepName,
|
|
204
183
|
command: `${file} ${args.join(" ")}`,
|
|
205
|
-
status:
|
|
206
|
-
durationMs
|
|
207
|
-
error: errorMsg
|
|
184
|
+
status: "success",
|
|
185
|
+
durationMs: 0
|
|
208
186
|
};
|
|
209
187
|
}
|
|
210
|
-
|
|
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
|
+
}
|
|
211
214
|
|
|
212
215
|
// src/managers/homebrew.ts
|
|
213
216
|
var HomebrewManager = class extends BasePackageManager {
|
|
@@ -421,15 +424,25 @@ var NpmManager = class extends BasePackageManager {
|
|
|
421
424
|
});
|
|
422
425
|
const updates = [];
|
|
423
426
|
const stdout = execRes.stdout.trim();
|
|
427
|
+
let npmError;
|
|
424
428
|
if (stdout.startsWith("{")) {
|
|
425
429
|
const parsed = JSON.parse(stdout);
|
|
426
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;
|
|
427
440
|
updates.push({
|
|
428
441
|
managerId: this.id,
|
|
429
442
|
managerName: this.name,
|
|
430
443
|
name,
|
|
431
|
-
currentVersion:
|
|
432
|
-
latestVersion:
|
|
444
|
+
currentVersion: entry.current ?? "unknown",
|
|
445
|
+
latestVersion: entry.latest ?? entry.wanted ?? "latest",
|
|
433
446
|
type: "global-pkg"
|
|
434
447
|
});
|
|
435
448
|
}
|
|
@@ -441,7 +454,8 @@ var NpmManager = class extends BasePackageManager {
|
|
|
441
454
|
category: this.category,
|
|
442
455
|
available: true,
|
|
443
456
|
updates,
|
|
444
|
-
durationMs: Date.now() - startTime
|
|
457
|
+
durationMs: Date.now() - startTime,
|
|
458
|
+
error: updates.length === 0 ? npmError : void 0
|
|
445
459
|
};
|
|
446
460
|
} catch (err) {
|
|
447
461
|
return {
|
|
@@ -1102,15 +1116,30 @@ var PipxManager = class extends BasePackageManager {
|
|
|
1102
1116
|
}
|
|
1103
1117
|
};
|
|
1104
1118
|
|
|
1105
|
-
// src/managers/
|
|
1106
|
-
var
|
|
1107
|
-
id = "
|
|
1108
|
-
name = "
|
|
1109
|
-
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}";
|
|
1110
1124
|
category = "language";
|
|
1111
|
-
binary = "
|
|
1112
|
-
|
|
1113
|
-
|
|
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;
|
|
1114
1143
|
}
|
|
1115
1144
|
async checkUpdates(options) {
|
|
1116
1145
|
const startTime = Date.now();
|
|
@@ -1127,49 +1156,36 @@ var CargoManager = class extends BasePackageManager {
|
|
|
1127
1156
|
};
|
|
1128
1157
|
}
|
|
1129
1158
|
try {
|
|
1159
|
+
const execRes = await safeExec("uv", ["tool", "list"], {
|
|
1160
|
+
timeoutMs: options?.timeoutMs ?? 15e3
|
|
1161
|
+
});
|
|
1130
1162
|
const updates = [];
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
name: `rustup (${toolchain})`,
|
|
1144
|
-
currentVersion: "installed",
|
|
1145
|
-
latestVersion: "update-available",
|
|
1146
|
-
type: "toolchain"
|
|
1147
|
-
});
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
}
|
|
1151
|
-
if (await commandExists("cargo-install-update")) {
|
|
1152
|
-
const listRes = await safeExec("cargo", ["install-update", "-l"], {
|
|
1153
|
-
timeoutMs: options?.timeoutMs ?? 15e3
|
|
1154
|
-
});
|
|
1155
|
-
const lines = listRes.stdout.split("\n");
|
|
1156
|
-
for (const line of lines) {
|
|
1157
|
-
const match = line.match(/^(\S+)\s+v(\S+)\s+v(\S+)/);
|
|
1158
|
-
if (match) {
|
|
1159
|
-
const [, name, cur, latest] = match;
|
|
1160
|
-
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) {
|
|
1161
1175
|
updates.push({
|
|
1162
1176
|
managerId: this.id,
|
|
1163
1177
|
managerName: this.name,
|
|
1164
1178
|
name,
|
|
1165
|
-
currentVersion:
|
|
1179
|
+
currentVersion: version,
|
|
1166
1180
|
latestVersion: latest,
|
|
1167
|
-
type: "
|
|
1181
|
+
type: "uv-tool"
|
|
1168
1182
|
});
|
|
1169
1183
|
}
|
|
1184
|
+
} catch {
|
|
1170
1185
|
}
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1186
|
+
})
|
|
1187
|
+
);
|
|
1188
|
+
updates.sort((a, b) => a.name.localeCompare(b.name));
|
|
1173
1189
|
return {
|
|
1174
1190
|
managerId: this.id,
|
|
1175
1191
|
managerName: this.name,
|
|
@@ -1195,26 +1211,29 @@ var CargoManager = class extends BasePackageManager {
|
|
|
1195
1211
|
async executeUpdate(items, options) {
|
|
1196
1212
|
const startTime = Date.now();
|
|
1197
1213
|
const steps = [];
|
|
1198
|
-
if (
|
|
1199
|
-
const
|
|
1200
|
-
|
|
1201
|
-
"
|
|
1202
|
-
["
|
|
1203
|
-
options
|
|
1204
|
-
);
|
|
1205
|
-
steps.push(rustupStep);
|
|
1206
|
-
}
|
|
1207
|
-
if (await commandExists("cargo-install-update")) {
|
|
1208
|
-
const cargoStep = await this.executeStep(
|
|
1209
|
-
"cargo install-update -a",
|
|
1210
|
-
"cargo",
|
|
1211
|
-
["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"],
|
|
1212
1219
|
options
|
|
1213
1220
|
);
|
|
1214
|
-
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
|
+
}
|
|
1215
1235
|
}
|
|
1216
|
-
const
|
|
1217
|
-
const success = !hasFailedStep;
|
|
1236
|
+
const success = steps.every((s) => s.status === "success");
|
|
1218
1237
|
return {
|
|
1219
1238
|
managerId: this.id,
|
|
1220
1239
|
managerName: this.name,
|
|
@@ -1228,22 +1247,17 @@ var CargoManager = class extends BasePackageManager {
|
|
|
1228
1247
|
}
|
|
1229
1248
|
};
|
|
1230
1249
|
|
|
1231
|
-
// src/managers/
|
|
1232
|
-
var
|
|
1233
|
-
id = "
|
|
1234
|
-
name = "
|
|
1235
|
-
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}";
|
|
1236
1255
|
category = "language";
|
|
1237
|
-
binary = "
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
const gemPath = whichRes.stdout.trim();
|
|
1243
|
-
if (gemPath === "/usr/bin/gem" || gemPath.startsWith("/System/Library")) {
|
|
1244
|
-
return false;
|
|
1245
|
-
}
|
|
1246
|
-
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];
|
|
1247
1261
|
}
|
|
1248
1262
|
async checkUpdates(options) {
|
|
1249
1263
|
const startTime = Date.now();
|
|
@@ -1260,24 +1274,31 @@ var RubyGemManager = class extends BasePackageManager {
|
|
|
1260
1274
|
};
|
|
1261
1275
|
}
|
|
1262
1276
|
try {
|
|
1263
|
-
const execRes = await safeExec("
|
|
1264
|
-
timeoutMs: options?.timeoutMs ??
|
|
1277
|
+
const execRes = await safeExec("poetry", ["--version"], {
|
|
1278
|
+
timeoutMs: options?.timeoutMs ?? 15e3
|
|
1265
1279
|
});
|
|
1266
1280
|
const updates = [];
|
|
1267
|
-
const
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
const [, name, current, latest] = match;
|
|
1273
|
-
updates.push({
|
|
1274
|
-
managerId: this.id,
|
|
1275
|
-
managerName: this.name,
|
|
1276
|
-
name: name.trim(),
|
|
1277
|
-
currentVersion: current.trim(),
|
|
1278
|
-
latestVersion: latest.trim(),
|
|
1279
|
-
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)
|
|
1280
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 {
|
|
1281
1302
|
}
|
|
1282
1303
|
}
|
|
1283
1304
|
return {
|
|
@@ -1305,20 +1326,15 @@ var RubyGemManager = class extends BasePackageManager {
|
|
|
1305
1326
|
async executeUpdate(items, options) {
|
|
1306
1327
|
const startTime = Date.now();
|
|
1307
1328
|
const steps = [];
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
"gem",
|
|
1318
|
-
["cleanup"],
|
|
1319
|
-
options
|
|
1320
|
-
);
|
|
1321
|
-
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
|
+
}
|
|
1322
1338
|
const success = steps.every((s) => s.status === "success");
|
|
1323
1339
|
return {
|
|
1324
1340
|
managerId: this.id,
|
|
@@ -1333,113 +1349,436 @@ var RubyGemManager = class extends BasePackageManager {
|
|
|
1333
1349
|
}
|
|
1334
1350
|
};
|
|
1335
1351
|
|
|
1336
|
-
// src/
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
if (!isAvail) {
|
|
1348
|
-
return {
|
|
1349
|
-
managerId: this.id,
|
|
1350
|
-
managerName: this.name,
|
|
1351
|
-
icon: this.icon,
|
|
1352
|
-
category: this.category,
|
|
1353
|
-
available: false,
|
|
1354
|
-
updates: [],
|
|
1355
|
-
durationMs: 0
|
|
1356
|
-
};
|
|
1357
|
-
}
|
|
1358
|
-
try {
|
|
1359
|
-
const execRes = await safeExec("port", ["outdated"], {
|
|
1360
|
-
timeoutMs: options?.timeoutMs ?? 2e4
|
|
1361
|
-
});
|
|
1362
|
-
const updates = [];
|
|
1363
|
-
const lines = execRes.stdout.split("\n");
|
|
1364
|
-
for (const line of lines) {
|
|
1365
|
-
const trimmed = line.trim();
|
|
1366
|
-
if (!trimmed || trimmed.startsWith("The following") || trimmed.startsWith("--")) {
|
|
1367
|
-
continue;
|
|
1368
|
-
}
|
|
1369
|
-
const match = trimmed.match(/^(\S+)\s+(\S+)\s*<\s*(\S+)/);
|
|
1370
|
-
if (match) {
|
|
1371
|
-
const [, name, current, latest] = match;
|
|
1372
|
-
updates.push({
|
|
1373
|
-
managerId: this.id,
|
|
1374
|
-
managerName: this.name,
|
|
1375
|
-
name,
|
|
1376
|
-
currentVersion: current,
|
|
1377
|
-
latestVersion: latest,
|
|
1378
|
-
type: "port"
|
|
1379
|
-
});
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
return {
|
|
1383
|
-
managerId: this.id,
|
|
1384
|
-
managerName: this.name,
|
|
1385
|
-
icon: this.icon,
|
|
1386
|
-
category: this.category,
|
|
1387
|
-
available: true,
|
|
1388
|
-
updates,
|
|
1389
|
-
durationMs: Date.now() - startTime
|
|
1390
|
-
};
|
|
1391
|
-
} catch (err) {
|
|
1392
|
-
return {
|
|
1393
|
-
managerId: this.id,
|
|
1394
|
-
managerName: this.name,
|
|
1395
|
-
icon: this.icon,
|
|
1396
|
-
category: this.category,
|
|
1397
|
-
available: true,
|
|
1398
|
-
updates: [],
|
|
1399
|
-
durationMs: Date.now() - startTime,
|
|
1400
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1401
|
-
};
|
|
1402
|
-
}
|
|
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.");
|
|
1403
1363
|
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
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
|
+
});
|
|
1423
1629
|
}
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
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
|
+
}
|
|
1427
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
|
+
};
|
|
1428
1763
|
}
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
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"],
|
|
1433
1772
|
options
|
|
1434
1773
|
);
|
|
1435
|
-
steps.push(
|
|
1436
|
-
const
|
|
1437
|
-
"
|
|
1438
|
-
"
|
|
1439
|
-
["
|
|
1774
|
+
steps.push(updateStep);
|
|
1775
|
+
const cleanupStep = await this.executeStep(
|
|
1776
|
+
"gem cleanup",
|
|
1777
|
+
"gem",
|
|
1778
|
+
["cleanup"],
|
|
1440
1779
|
options
|
|
1441
1780
|
);
|
|
1442
|
-
steps.push(
|
|
1781
|
+
steps.push(cleanupStep);
|
|
1443
1782
|
const success = steps.every((s) => s.status === "success");
|
|
1444
1783
|
return {
|
|
1445
1784
|
managerId: this.id,
|
|
@@ -1454,13 +1793,14 @@ var MacPortsManager = class extends BasePackageManager {
|
|
|
1454
1793
|
}
|
|
1455
1794
|
};
|
|
1456
1795
|
|
|
1457
|
-
// src/managers/
|
|
1458
|
-
var
|
|
1459
|
-
id = "
|
|
1460
|
-
name = "
|
|
1461
|
-
icon = "\
|
|
1462
|
-
category = "
|
|
1463
|
-
|
|
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";
|
|
1464
1804
|
async checkUpdates(options) {
|
|
1465
1805
|
const startTime = Date.now();
|
|
1466
1806
|
const isAvail = await this.isAvailable();
|
|
@@ -1476,23 +1816,26 @@ var MacAppStoreManager = class extends BasePackageManager {
|
|
|
1476
1816
|
};
|
|
1477
1817
|
}
|
|
1478
1818
|
try {
|
|
1479
|
-
const execRes = await safeExec("
|
|
1819
|
+
const execRes = await safeExec("port", ["outdated"], {
|
|
1480
1820
|
timeoutMs: options?.timeoutMs ?? 2e4
|
|
1481
1821
|
});
|
|
1482
1822
|
const updates = [];
|
|
1483
1823
|
const lines = execRes.stdout.split("\n");
|
|
1484
1824
|
for (const line of lines) {
|
|
1485
1825
|
const trimmed = line.trim();
|
|
1486
|
-
|
|
1826
|
+
if (!trimmed || trimmed.startsWith("The following") || trimmed.startsWith("--")) {
|
|
1827
|
+
continue;
|
|
1828
|
+
}
|
|
1829
|
+
const match = trimmed.match(/^(\S+)\s+(\S+)\s*<\s*(\S+)/);
|
|
1487
1830
|
if (match) {
|
|
1488
|
-
const [,
|
|
1831
|
+
const [, name, current, latest] = match;
|
|
1489
1832
|
updates.push({
|
|
1490
1833
|
managerId: this.id,
|
|
1491
1834
|
managerName: this.name,
|
|
1492
|
-
name
|
|
1493
|
-
currentVersion: current
|
|
1494
|
-
latestVersion: latest
|
|
1495
|
-
type: "
|
|
1835
|
+
name,
|
|
1836
|
+
currentVersion: current,
|
|
1837
|
+
latestVersion: latest,
|
|
1838
|
+
type: "port"
|
|
1496
1839
|
});
|
|
1497
1840
|
}
|
|
1498
1841
|
}
|
|
@@ -1521,10 +1864,39 @@ var MacAppStoreManager = class extends BasePackageManager {
|
|
|
1521
1864
|
async executeUpdate(items, options) {
|
|
1522
1865
|
const startTime = Date.now();
|
|
1523
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,
|
|
1872
|
+
managerName: this.name,
|
|
1873
|
+
icon: this.icon,
|
|
1874
|
+
success: false,
|
|
1875
|
+
updatedCount: 0,
|
|
1876
|
+
durationMs: Date.now() - startTime,
|
|
1877
|
+
steps: [
|
|
1878
|
+
{
|
|
1879
|
+
name: "sudo privileges",
|
|
1880
|
+
command: "sudo -n true",
|
|
1881
|
+
status: "failed",
|
|
1882
|
+
error
|
|
1883
|
+
}
|
|
1884
|
+
],
|
|
1885
|
+
error
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
const selfupdateStep = await this.executeStep(
|
|
1890
|
+
"sudo port selfupdate",
|
|
1891
|
+
"sudo",
|
|
1892
|
+
["port", "selfupdate"],
|
|
1893
|
+
options
|
|
1894
|
+
);
|
|
1895
|
+
steps.push(selfupdateStep);
|
|
1524
1896
|
const upgradeStep = await this.executeStep(
|
|
1525
|
-
"
|
|
1526
|
-
"
|
|
1527
|
-
["upgrade"],
|
|
1897
|
+
"sudo port upgrade outdated",
|
|
1898
|
+
"sudo",
|
|
1899
|
+
["port", "upgrade", "outdated"],
|
|
1528
1900
|
options
|
|
1529
1901
|
);
|
|
1530
1902
|
steps.push(upgradeStep);
|
|
@@ -1542,100 +1914,93 @@ var MacAppStoreManager = class extends BasePackageManager {
|
|
|
1542
1914
|
}
|
|
1543
1915
|
};
|
|
1544
1916
|
|
|
1545
|
-
// src/
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
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])}`);
|
|
1917
|
+
// src/managers/mas.ts
|
|
1918
|
+
var MacAppStoreManager = class extends BasePackageManager {
|
|
1919
|
+
id = "mas";
|
|
1920
|
+
name = "Mac App Store";
|
|
1921
|
+
icon = "\u{1F34E}";
|
|
1922
|
+
category = "appstore";
|
|
1923
|
+
binary = "mas";
|
|
1924
|
+
async checkUpdates(options) {
|
|
1925
|
+
const startTime = Date.now();
|
|
1926
|
+
const isAvail = await this.isAvailable();
|
|
1927
|
+
if (!isAvail) {
|
|
1928
|
+
return {
|
|
1929
|
+
managerId: this.id,
|
|
1930
|
+
managerName: this.name,
|
|
1931
|
+
icon: this.icon,
|
|
1932
|
+
category: this.category,
|
|
1933
|
+
available: false,
|
|
1934
|
+
updates: [],
|
|
1935
|
+
durationMs: 0
|
|
1936
|
+
};
|
|
1618
1937
|
}
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1938
|
+
try {
|
|
1939
|
+
const execRes = await safeExec("mas", ["outdated"], {
|
|
1940
|
+
timeoutMs: options?.timeoutMs ?? 2e4
|
|
1941
|
+
});
|
|
1942
|
+
const updates = [];
|
|
1943
|
+
const lines = execRes.stdout.split("\n");
|
|
1944
|
+
for (const line of lines) {
|
|
1945
|
+
const trimmed = line.trim();
|
|
1946
|
+
const match = trimmed.match(/^\s*(\d+)\s+(.+?)\s*\(([^->]+)->\s*([^)]+)\)/);
|
|
1947
|
+
if (match) {
|
|
1948
|
+
const [, id, appName, current, latest] = match;
|
|
1949
|
+
updates.push({
|
|
1950
|
+
managerId: this.id,
|
|
1951
|
+
managerName: this.name,
|
|
1952
|
+
name: `${appName.trim()} [${id}]`,
|
|
1953
|
+
currentVersion: current.trim(),
|
|
1954
|
+
latestVersion: latest.trim(),
|
|
1955
|
+
type: "mas-app"
|
|
1956
|
+
});
|
|
1957
|
+
}
|
|
1626
1958
|
}
|
|
1959
|
+
return {
|
|
1960
|
+
managerId: this.id,
|
|
1961
|
+
managerName: this.name,
|
|
1962
|
+
icon: this.icon,
|
|
1963
|
+
category: this.category,
|
|
1964
|
+
available: true,
|
|
1965
|
+
updates,
|
|
1966
|
+
durationMs: Date.now() - startTime
|
|
1967
|
+
};
|
|
1968
|
+
} catch (err) {
|
|
1969
|
+
return {
|
|
1970
|
+
managerId: this.id,
|
|
1971
|
+
managerName: this.name,
|
|
1972
|
+
icon: this.icon,
|
|
1973
|
+
category: this.category,
|
|
1974
|
+
available: true,
|
|
1975
|
+
updates: [],
|
|
1976
|
+
durationMs: Date.now() - startTime,
|
|
1977
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1978
|
+
};
|
|
1627
1979
|
}
|
|
1628
1980
|
}
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1981
|
+
async executeUpdate(items, options) {
|
|
1982
|
+
const startTime = Date.now();
|
|
1983
|
+
const steps = [];
|
|
1984
|
+
const upgradeStep = await this.executeStep(
|
|
1985
|
+
"mas upgrade",
|
|
1986
|
+
"mas",
|
|
1987
|
+
["upgrade"],
|
|
1988
|
+
options
|
|
1989
|
+
);
|
|
1990
|
+
steps.push(upgradeStep);
|
|
1991
|
+
const success = steps.every((s) => s.status === "success");
|
|
1992
|
+
return {
|
|
1993
|
+
managerId: this.id,
|
|
1994
|
+
managerName: this.name,
|
|
1995
|
+
icon: this.icon,
|
|
1996
|
+
success,
|
|
1997
|
+
updatedCount: success ? items.length : 0,
|
|
1998
|
+
durationMs: Date.now() - startTime,
|
|
1999
|
+
steps,
|
|
2000
|
+
error: !success ? steps.find((s) => s.status === "failed")?.error : void 0
|
|
2001
|
+
};
|
|
2002
|
+
}
|
|
2003
|
+
};
|
|
1639
2004
|
|
|
1640
2005
|
// src/managers/linux.ts
|
|
1641
2006
|
var AptManager = class extends BasePackageManager {
|
|
@@ -2129,6 +2494,170 @@ var DnfManager = class extends BasePackageManager {
|
|
|
2129
2494
|
}
|
|
2130
2495
|
};
|
|
2131
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
|
+
|
|
2132
2661
|
// src/managers/registry.ts
|
|
2133
2662
|
function createDefaultManagers() {
|
|
2134
2663
|
return [
|
|
@@ -2139,13 +2668,17 @@ function createDefaultManagers() {
|
|
|
2139
2668
|
new YarnManager(),
|
|
2140
2669
|
new PythonPipManager(),
|
|
2141
2670
|
new PipxManager(),
|
|
2671
|
+
new UvManager(),
|
|
2672
|
+
new PoetryManager(),
|
|
2673
|
+
new CondaManager(),
|
|
2142
2674
|
new CargoManager(),
|
|
2143
2675
|
new RubyGemManager(),
|
|
2144
2676
|
new MacPortsManager(),
|
|
2145
2677
|
new MacAppStoreManager(),
|
|
2146
2678
|
new AptManager(),
|
|
2147
2679
|
new DnfManager(),
|
|
2148
|
-
new FlatpakManager()
|
|
2680
|
+
new FlatpakManager(),
|
|
2681
|
+
new FirmwareManager()
|
|
2149
2682
|
];
|
|
2150
2683
|
}
|
|
2151
2684
|
function filterManagers(managers, only, exclude) {
|
|
@@ -2281,10 +2814,260 @@ var UpdaterEngine = class {
|
|
|
2281
2814
|
}
|
|
2282
2815
|
};
|
|
2283
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
|
+
|
|
2284
3067
|
// src/utils/version.ts
|
|
2285
3068
|
import { fileURLToPath } from "url";
|
|
2286
3069
|
import { dirname, resolve } from "path";
|
|
2287
|
-
import { existsSync, readFileSync } from "fs";
|
|
3070
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
2288
3071
|
var FALLBACK_METADATA = {
|
|
2289
3072
|
name: "@dahch/heimdall",
|
|
2290
3073
|
version: "1.0.1",
|
|
@@ -2294,7 +3077,7 @@ var cachedMetadata = null;
|
|
|
2294
3077
|
var customFsAdapter = null;
|
|
2295
3078
|
function resolvePackageMetadata(options) {
|
|
2296
3079
|
const exists = options?.fsAdapter?.existsSync ?? customFsAdapter?.existsSync ?? existsSync;
|
|
2297
|
-
const read = options?.fsAdapter?.readFileSync ?? customFsAdapter?.readFileSync ??
|
|
3080
|
+
const read = options?.fsAdapter?.readFileSync ?? customFsAdapter?.readFileSync ?? readFileSync2;
|
|
2298
3081
|
try {
|
|
2299
3082
|
let currentDir;
|
|
2300
3083
|
if (options?.basePath) {
|
|
@@ -2347,10 +3130,127 @@ function getPackageMetadata(options) {
|
|
|
2347
3130
|
}
|
|
2348
3131
|
|
|
2349
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
|
+
}
|
|
2350
3247
|
async function main() {
|
|
2351
3248
|
const { version, description } = getPackageMetadata();
|
|
2352
3249
|
const program = new Command();
|
|
2353
|
-
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);
|
|
2354
3254
|
const options = program.opts();
|
|
2355
3255
|
if (process.stdout.isTTY && !options.verbose) {
|
|
2356
3256
|
console.clear();
|
|
@@ -2396,8 +3296,9 @@ async function main() {
|
|
|
2396
3296
|
pc2.green("All detected package managers and global packages are completely up to date! \u{1F389}"),
|
|
2397
3297
|
"Status: Up to Date"
|
|
2398
3298
|
);
|
|
2399
|
-
|
|
2400
|
-
|
|
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);
|
|
2401
3302
|
}
|
|
2402
3303
|
p.log.message(`
|
|
2403
3304
|
${pc2.bold("Pending updates")} ${pc2.dim(`(${allUpdates.length} total):`)}`);
|
|
@@ -2543,7 +3444,8 @@ ${isSimulated ? "[DRY RUN] Simulating" : "Starting"} updates for ${managersToUpd
|
|
|
2543
3444
|
stopSudoKeepAlive?.();
|
|
2544
3445
|
}
|
|
2545
3446
|
console.log(renderExecutionSummary(summary.results));
|
|
2546
|
-
|
|
3447
|
+
const distroOk = await offerDistroUpgrade(options, timeoutMs, isSimulated);
|
|
3448
|
+
if (summary.failedCount === 0 && distroOk) {
|
|
2547
3449
|
p.outro(
|
|
2548
3450
|
pc2.bold(
|
|
2549
3451
|
pc2.green(
|
|
@@ -2553,10 +3455,11 @@ ${isSimulated ? "[DRY RUN] Simulating" : "Starting"} updates for ${managersToUpd
|
|
|
2553
3455
|
);
|
|
2554
3456
|
} else {
|
|
2555
3457
|
process.exitCode = 1;
|
|
3458
|
+
const distroNote = distroOk ? "" : ", distribution upgrade failed";
|
|
2556
3459
|
p.outro(
|
|
2557
3460
|
pc2.bold(
|
|
2558
3461
|
pc2.yellow(
|
|
2559
|
-
`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)}).`
|
|
2560
3463
|
)
|
|
2561
3464
|
)
|
|
2562
3465
|
);
|