@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.
Files changed (3) hide show
  1. package/README.md +49 -3
  2. package/dist/cli.js +1257 -354
  3. 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
- const startTime = Date.now();
173
- options.onStepStart?.(stepName);
174
- if (options.dryRun) {
175
- options.onStepProgress?.(stepName, `[DRY-RUN] Would run: ${file} ${args.join(" ")}`);
176
- options.onStepEnd?.(stepName, true);
177
- return {
178
- name: stepName,
179
- command: `${file} ${args.join(" ")}`,
180
- status: "success",
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: success ? "success" : "failed",
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: info.current ?? "unknown",
432
- latestVersion: info.latest ?? info.wanted ?? "latest",
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/cargo.ts
1106
- var CargoManager = class extends BasePackageManager {
1107
- id = "cargo";
1108
- name = "Rust (Cargo & Rustup)";
1109
- icon = "\u{1F980}";
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 = "cargo";
1112
- async isAvailable() {
1113
- return await commandExists("cargo") || await commandExists("rustup");
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
- if (await commandExists("rustup")) {
1132
- const checkRes = await safeExec("rustup", ["check"], {
1133
- timeoutMs: options?.timeoutMs ?? 15e3
1134
- });
1135
- const lines = checkRes.stdout.split("\n");
1136
- for (const line of lines) {
1137
- if (line.includes("Update available")) {
1138
- const parts = line.trim().split(/\s+/);
1139
- const toolchain = parts[0] ?? "toolchain";
1140
- updates.push({
1141
- managerId: this.id,
1142
- managerName: this.name,
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: cur,
1179
+ currentVersion: version,
1166
1180
  latestVersion: latest,
1167
- type: "crate"
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 (await commandExists("rustup")) {
1199
- const rustupStep = await this.executeStep(
1200
- "rustup update",
1201
- "rustup",
1202
- ["update"],
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(cargoStep);
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 hasFailedStep = steps.some((s) => s.status === "failed");
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/gem.ts
1232
- var RubyGemManager = class extends BasePackageManager {
1233
- id = "gem";
1234
- name = "Ruby Gem";
1235
- icon = "\u{1F48E}";
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 = "gem";
1238
- async isAvailable() {
1239
- const hasCmd = await commandExists(this.binary);
1240
- if (!hasCmd) return false;
1241
- const whichRes = await safeExec("which", ["gem"], { timeoutMs: 3e3 });
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("gem", ["outdated"], {
1264
- timeoutMs: options?.timeoutMs ?? 2e4
1277
+ const execRes = await safeExec("poetry", ["--version"], {
1278
+ timeoutMs: options?.timeoutMs ?? 15e3
1265
1279
  });
1266
1280
  const updates = [];
1267
- const lines = execRes.stdout.split("\n");
1268
- for (const line of lines) {
1269
- const trimmed = line.trim();
1270
- const match = trimmed.match(/^(\S+)\s*\(([^<]+)<\s*([^)]+)\)/);
1271
- if (match) {
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
- const updateStep = await this.executeStep(
1309
- "gem update --user-install",
1310
- "gem",
1311
- ["update", "--user-install"],
1312
- options
1313
- );
1314
- steps.push(updateStep);
1315
- const cleanupStep = await this.executeStep(
1316
- "gem cleanup",
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/managers/macports.ts
1337
- var MacPortsManager = class extends BasePackageManager {
1338
- id = "macports";
1339
- name = "MacPorts";
1340
- icon = "\u2693";
1341
- category = "system";
1342
- requiresSudo = true;
1343
- binary = "port";
1344
- async checkUpdates(options) {
1345
- const startTime = Date.now();
1346
- const isAvail = await this.isAvailable();
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
- async executeUpdate(items, options) {
1405
- const startTime = Date.now();
1406
- const steps = [];
1407
- if (!options.dryRun && process.getuid?.() !== 0) {
1408
- if (!await this.hasPasswordlessSudo()) {
1409
- const error = `MacPorts requires sudo privileges. ${SUDO_REQUIRED_HINT}`;
1410
- return {
1411
- managerId: this.id,
1412
- managerName: this.name,
1413
- icon: this.icon,
1414
- success: false,
1415
- updatedCount: 0,
1416
- durationMs: Date.now() - startTime,
1417
- steps: [
1418
- {
1419
- name: "sudo privileges",
1420
- command: "sudo -n true",
1421
- status: "failed",
1422
- error
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
- error
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
- const selfupdateStep = await this.executeStep(
1430
- "sudo port selfupdate",
1431
- "sudo",
1432
- ["port", "selfupdate"],
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(selfupdateStep);
1436
- const upgradeStep = await this.executeStep(
1437
- "sudo port upgrade outdated",
1438
- "sudo",
1439
- ["port", "upgrade", "outdated"],
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(upgradeStep);
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/mas.ts
1458
- var MacAppStoreManager = class extends BasePackageManager {
1459
- id = "mas";
1460
- name = "Mac App Store";
1461
- icon = "\u{1F34E}";
1462
- category = "appstore";
1463
- binary = "mas";
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("mas", ["outdated"], {
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
- const match = trimmed.match(/^\s*(\d+)\s+(.+?)\s*\(([^->]+)->\s*([^)]+)\)/);
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 [, id, appName, current, latest] = match;
1831
+ const [, name, current, latest] = match;
1489
1832
  updates.push({
1490
1833
  managerId: this.id,
1491
1834
  managerName: this.name,
1492
- name: `${appName.trim()} [${id}]`,
1493
- currentVersion: current.trim(),
1494
- latestVersion: latest.trim(),
1495
- type: "mas-app"
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
- "mas upgrade",
1526
- "mas",
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/utils/formatting.ts
1546
- import pc from "picocolors";
1547
- import Table from "cli-table3";
1548
- function formatDuration(ms) {
1549
- if (ms < 1e3) return `${ms}ms`;
1550
- const seconds = (ms / 1e3).toFixed(1);
1551
- return `${seconds}s`;
1552
- }
1553
- function renderUpdatesTable(items) {
1554
- if (items.length === 0) {
1555
- return pc.dim(" No updates pending.");
1556
- }
1557
- const table = new Table({
1558
- head: [
1559
- pc.dim("Manager"),
1560
- pc.dim("Package"),
1561
- pc.dim("Type"),
1562
- pc.dim("Version Transition")
1563
- ],
1564
- style: {
1565
- head: [],
1566
- border: ["dim"]
1567
- },
1568
- chars: {
1569
- top: "\u2500",
1570
- "top-mid": "\u2500",
1571
- "top-left": "\u250C",
1572
- "top-right": "\u2510",
1573
- bottom: "\u2500",
1574
- "bottom-mid": "\u2500",
1575
- "bottom-left": "\u2514",
1576
- "bottom-right": "\u2518",
1577
- left: "\u2502",
1578
- "left-mid": "\u251C",
1579
- mid: "\u2500",
1580
- "mid-mid": "\u253C",
1581
- right: "\u2502",
1582
- "right-mid": "\u2524",
1583
- middle: "\u2502"
1584
- }
1585
- });
1586
- for (const item of items) {
1587
- const typeLabel = item.type ? pc.dim(`[${item.type}]`) : pc.dim("-");
1588
- const currentVer = pc.yellow(item.currentVersion || "?");
1589
- const arrow = pc.dim(" \u2192 ");
1590
- const latestVer = pc.green(pc.bold(item.latestVersion || "?"));
1591
- table.push([
1592
- pc.cyan(item.managerName),
1593
- pc.bold(pc.white(item.name)),
1594
- typeLabel,
1595
- `${currentVer}${arrow}${latestVer}`
1596
- ]);
1597
- }
1598
- return table.toString();
1599
- }
1600
- function renderExecutionSummary(results) {
1601
- const total = results.length;
1602
- const successful = results.filter((r) => r.success).length;
1603
- const failed = results.filter((r) => !r.success).length;
1604
- const totalUpdated = results.reduce((acc, r) => acc + r.updatedCount, 0);
1605
- const lines = [];
1606
- lines.push("");
1607
- lines.push(pc.dim("\u2500".repeat(60)));
1608
- lines.push(` ${pc.bold(pc.cyan("Heimdall"))} ${pc.dim("\u2022 Execution Summary")}`);
1609
- lines.push(pc.dim("\u2500".repeat(60)));
1610
- for (const res of results) {
1611
- const icon = res.icon || "\u{1F4E6}";
1612
- const status = res.success ? pc.green("\u2714 SUCCESS") : pc.red("\u2716 FAILED");
1613
- const time = pc.dim(`(${formatDuration(res.durationMs)})`);
1614
- const count = res.success ? pc.dim(`\u2022 ${res.updatedCount} updated`) : pc.dim(`\u2022 ${res.updatedCount} updated, ${res.steps.filter((s) => s.status === "failed").length} issue(s)`);
1615
- lines.push(` ${icon} ${pc.bold(res.managerName)} ${status} ${count} ${time}`);
1616
- if (!res.success && res.error) {
1617
- lines.push(` ${pc.red("\u21B3 Error:")} ${pc.dim(res.error.trim().split("\n")[0])}`);
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
- if (res.steps.length > 0) {
1620
- for (const step of res.steps) {
1621
- if (step.status === "skipped") continue;
1622
- const stepStatus = step.status === "success" ? pc.green("\u2714") : step.status === "failed" ? pc.red("\u2716") : pc.dim("\u25CB");
1623
- const stepDuration = step.durationMs ? pc.dim(`[${formatDuration(step.durationMs)}]`) : "";
1624
- const stepError = step.error && step.status === "failed" ? pc.red(` (${step.error.split("\n")[0]})`) : "";
1625
- lines.push(` ${stepStatus} ${pc.dim(step.name)} ${stepDuration}${stepError}`);
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
- lines.push(pc.dim("\u2500".repeat(60)));
1630
- const summaryParts = [
1631
- pc.green(`${successful}/${total} managers succeeded`),
1632
- failed > 0 ? pc.red(`${failed} failed`) : null,
1633
- pc.cyan(`${totalUpdated} package(s) updated`)
1634
- ].filter(Boolean);
1635
- lines.push(` ${summaryParts.join(" \u2022 ")}`);
1636
- lines.push(pc.dim("\u2500".repeat(60)));
1637
- return lines.join("\n");
1638
- }
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 ?? 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").parse(process.argv);
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
- p.outro(pc2.cyan("No updates needed. Have a great day!"));
2400
- process.exit(0);
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
- if (summary.failedCount === 0) {
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
  );