@dahch/heimdall 1.0.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 +188 -0
- package/dist/cli.js +2227 -0
- package/package.json +61 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2227 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import * as p from "@clack/prompts";
|
|
6
|
+
import pc2 from "picocolors";
|
|
7
|
+
|
|
8
|
+
// src/utils/exec.ts
|
|
9
|
+
import { execa } from "execa";
|
|
10
|
+
async function commandExists(command) {
|
|
11
|
+
try {
|
|
12
|
+
const result = await execa("which", [command], {
|
|
13
|
+
reject: false,
|
|
14
|
+
timeout: 3e3
|
|
15
|
+
});
|
|
16
|
+
return result.exitCode === 0;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function safeExec(file, args = [], options = {}) {
|
|
22
|
+
const timeout = options.timeoutMs ?? 3e4;
|
|
23
|
+
try {
|
|
24
|
+
const subprocess = execa(file, args, {
|
|
25
|
+
timeout,
|
|
26
|
+
cwd: options.cwd,
|
|
27
|
+
env: {
|
|
28
|
+
...process.env,
|
|
29
|
+
...options.env,
|
|
30
|
+
// Suppress interactive prompts in subcommands
|
|
31
|
+
CI: "true",
|
|
32
|
+
DEBIAN_FRONTEND: "noninteractive"
|
|
33
|
+
},
|
|
34
|
+
reject: false,
|
|
35
|
+
stdin: options.stdin ?? "ignore"
|
|
36
|
+
});
|
|
37
|
+
if (options.onData && subprocess.stdout) {
|
|
38
|
+
subprocess.stdout.on("data", (data) => {
|
|
39
|
+
options.onData?.(data.toString());
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
if (options.onData && subprocess.stderr) {
|
|
43
|
+
subprocess.stderr.on("data", (data) => {
|
|
44
|
+
options.onData?.(data.toString());
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const result = await subprocess;
|
|
48
|
+
return {
|
|
49
|
+
stdout: (result.stdout ?? "").toString(),
|
|
50
|
+
stderr: (result.stderr ?? "").toString(),
|
|
51
|
+
exitCode: result.exitCode ?? 0,
|
|
52
|
+
success: result.exitCode === 0,
|
|
53
|
+
timedOut: result.timedOut ?? false
|
|
54
|
+
};
|
|
55
|
+
} catch (err) {
|
|
56
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
57
|
+
const isTimeout = error.name === "TimeoutError" || error.message.includes("timed out");
|
|
58
|
+
return {
|
|
59
|
+
stdout: "",
|
|
60
|
+
stderr: error.message,
|
|
61
|
+
exitCode: -1,
|
|
62
|
+
success: false,
|
|
63
|
+
timedOut: isTimeout,
|
|
64
|
+
error
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/utils/error.ts
|
|
70
|
+
var ANSI_REGEX = /\u001b\[[0-9;?]*[a-zA-Z]/g;
|
|
71
|
+
function extractErrorMessage(rawError) {
|
|
72
|
+
if (!rawError) {
|
|
73
|
+
return "Command exited with error";
|
|
74
|
+
}
|
|
75
|
+
const stripped = rawError.replace(ANSI_REGEX, "").trim();
|
|
76
|
+
if (!stripped) {
|
|
77
|
+
return "Command exited with error";
|
|
78
|
+
}
|
|
79
|
+
const lines = stripped.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
80
|
+
if (lines.length === 0) {
|
|
81
|
+
return "Command exited with error";
|
|
82
|
+
}
|
|
83
|
+
const isWarningOrNotice = (line) => {
|
|
84
|
+
const lower = line.toLowerCase();
|
|
85
|
+
return lower.startsWith("npm warn") || lower.startsWith("npm notice") || lower.startsWith("warning:") || lower.startsWith("[warn]") || lower.startsWith("warn:") || lower.startsWith("npm info");
|
|
86
|
+
};
|
|
87
|
+
const filteredLines = lines.filter((line) => !isWarningOrNotice(line));
|
|
88
|
+
if (filteredLines.length === 0) {
|
|
89
|
+
return "Command exited with error";
|
|
90
|
+
}
|
|
91
|
+
const npmErrorLines = [];
|
|
92
|
+
const otherErrorLines = [];
|
|
93
|
+
for (const line of filteredLines) {
|
|
94
|
+
const lower = line.toLowerCase();
|
|
95
|
+
if (lower.startsWith("npm error") || lower.startsWith("npm err!")) {
|
|
96
|
+
const cleaned = line.replace(/^npm\s+(?:error|err!)\s*:?\s*/i, "").trim();
|
|
97
|
+
if (cleaned) {
|
|
98
|
+
npmErrorLines.push(cleaned);
|
|
99
|
+
}
|
|
100
|
+
} else if (line.includes("error:") || line.includes("Error:") || line.includes("fatal:")) {
|
|
101
|
+
otherErrorLines.push(line);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
let result = "";
|
|
105
|
+
if (npmErrorLines.length > 0) {
|
|
106
|
+
const specificLines = npmErrorLines.filter((l) => {
|
|
107
|
+
const low = l.toLowerCase();
|
|
108
|
+
return !low.startsWith("code ") && !low.startsWith("errno ") && !low.startsWith("syscall ") && !low.startsWith("path ") && !low.startsWith("a complete log of this run");
|
|
109
|
+
});
|
|
110
|
+
if (specificLines.length === 0) {
|
|
111
|
+
result = npmErrorLines[0];
|
|
112
|
+
} else if (specificLines.length === 1) {
|
|
113
|
+
result = specificLines[0];
|
|
114
|
+
} else {
|
|
115
|
+
const hasCommandFailed = specificLines[0].toLowerCase().startsWith("command failed");
|
|
116
|
+
if (hasCommandFailed && specificLines.length > 1) {
|
|
117
|
+
const rest = specificLines.slice(1).map((l) => l.replace(/^command\s+/i, "")).join("; ");
|
|
118
|
+
result = `command failed: ${rest}`;
|
|
119
|
+
} else {
|
|
120
|
+
result = specificLines.join("; ");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} else if (otherErrorLines.length > 0) {
|
|
124
|
+
result = otherErrorLines.join("; ");
|
|
125
|
+
} else {
|
|
126
|
+
result = filteredLines[filteredLines.length - 1];
|
|
127
|
+
}
|
|
128
|
+
result = result.replace(/[\{\[\:\,\s]+$/, "").trim();
|
|
129
|
+
if (!result) {
|
|
130
|
+
return "Command exited with error";
|
|
131
|
+
}
|
|
132
|
+
if (result.length > 120) {
|
|
133
|
+
return result.slice(0, 117).trim() + "...";
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/managers/base.ts
|
|
139
|
+
var BasePackageManager = class {
|
|
140
|
+
async isAvailable() {
|
|
141
|
+
return commandExists(this.binary);
|
|
142
|
+
}
|
|
143
|
+
async executeStep(stepName, file, args, options, extraExecOptions) {
|
|
144
|
+
const startTime = Date.now();
|
|
145
|
+
options.onStepStart?.(stepName);
|
|
146
|
+
if (options.dryRun) {
|
|
147
|
+
options.onStepProgress?.(stepName, `[DRY-RUN] Would run: ${file} ${args.join(" ")}`);
|
|
148
|
+
options.onStepEnd?.(stepName, true);
|
|
149
|
+
return {
|
|
150
|
+
name: stepName,
|
|
151
|
+
command: `${file} ${args.join(" ")}`,
|
|
152
|
+
status: "success",
|
|
153
|
+
durationMs: 0
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const result = await safeExec(file, args, {
|
|
157
|
+
...extraExecOptions,
|
|
158
|
+
timeoutMs: options.timeoutMs ?? 18e4,
|
|
159
|
+
// 3 min per step
|
|
160
|
+
onData: (data) => {
|
|
161
|
+
if (options.onStepProgress) {
|
|
162
|
+
const lines = data.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
163
|
+
if (lines.length > 0) {
|
|
164
|
+
options.onStepProgress(stepName, lines[lines.length - 1]);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
const durationMs = Date.now() - startTime;
|
|
170
|
+
const success = result.success;
|
|
171
|
+
const rawOutput = [result.stderr, result.stdout].filter(Boolean).join("\n").trim();
|
|
172
|
+
const errorMsg = !success ? result.timedOut ? `Step timed out after ${Math.round((options.timeoutMs ?? 18e4) / 1e3)}s` : extractErrorMessage(rawOutput) : void 0;
|
|
173
|
+
options.onStepEnd?.(stepName, success, errorMsg, durationMs);
|
|
174
|
+
return {
|
|
175
|
+
name: stepName,
|
|
176
|
+
command: `${file} ${args.join(" ")}`,
|
|
177
|
+
status: success ? "success" : "failed",
|
|
178
|
+
durationMs,
|
|
179
|
+
error: errorMsg
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// src/managers/homebrew.ts
|
|
185
|
+
var HomebrewManager = class extends BasePackageManager {
|
|
186
|
+
id = "homebrew";
|
|
187
|
+
name = "Homebrew";
|
|
188
|
+
icon = "\u{1F37A}";
|
|
189
|
+
category = "system";
|
|
190
|
+
binary = "brew";
|
|
191
|
+
async checkUpdates(options) {
|
|
192
|
+
const startTime = Date.now();
|
|
193
|
+
const isAvail = await this.isAvailable();
|
|
194
|
+
if (!isAvail) {
|
|
195
|
+
return {
|
|
196
|
+
managerId: this.id,
|
|
197
|
+
managerName: this.name,
|
|
198
|
+
icon: this.icon,
|
|
199
|
+
category: this.category,
|
|
200
|
+
available: false,
|
|
201
|
+
updates: [],
|
|
202
|
+
durationMs: 0
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const execRes = await safeExec("brew", ["outdated", "--json=v2"], {
|
|
207
|
+
timeoutMs: options?.timeoutMs ?? 3e4
|
|
208
|
+
});
|
|
209
|
+
if (!execRes.success) {
|
|
210
|
+
return {
|
|
211
|
+
managerId: this.id,
|
|
212
|
+
managerName: this.name,
|
|
213
|
+
icon: this.icon,
|
|
214
|
+
category: this.category,
|
|
215
|
+
available: true,
|
|
216
|
+
updates: [],
|
|
217
|
+
durationMs: Date.now() - startTime,
|
|
218
|
+
error: extractErrorMessage(execRes.stderr || execRes.stdout)
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
const updates = [];
|
|
222
|
+
if (execRes.stdout.trim()) {
|
|
223
|
+
try {
|
|
224
|
+
const parsed = JSON.parse(execRes.stdout);
|
|
225
|
+
if (Array.isArray(parsed.formulae)) {
|
|
226
|
+
for (const f of parsed.formulae) {
|
|
227
|
+
updates.push({
|
|
228
|
+
managerId: this.id,
|
|
229
|
+
managerName: this.name,
|
|
230
|
+
name: f.name,
|
|
231
|
+
currentVersion: f.installed_versions?.[0] ?? "unknown",
|
|
232
|
+
latestVersion: f.current_version ?? "latest",
|
|
233
|
+
type: "formula"
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (Array.isArray(parsed.casks)) {
|
|
238
|
+
for (const c of parsed.casks) {
|
|
239
|
+
const cur = Array.isArray(c.installed_versions) ? c.installed_versions[0] : c.installed_versions;
|
|
240
|
+
updates.push({
|
|
241
|
+
managerId: this.id,
|
|
242
|
+
managerName: this.name,
|
|
243
|
+
name: c.name,
|
|
244
|
+
currentVersion: cur ?? "unknown",
|
|
245
|
+
latestVersion: c.current_version ?? "latest",
|
|
246
|
+
type: "cask"
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
} catch {
|
|
251
|
+
const textRes = await safeExec("brew", ["outdated"], { timeoutMs: 15e3 });
|
|
252
|
+
if (textRes.stdout.trim()) {
|
|
253
|
+
const lines = textRes.stdout.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
254
|
+
for (const line of lines) {
|
|
255
|
+
const parts = line.split(/\s+/);
|
|
256
|
+
updates.push({
|
|
257
|
+
managerId: this.id,
|
|
258
|
+
managerName: this.name,
|
|
259
|
+
name: parts[0],
|
|
260
|
+
currentVersion: parts[1] ?? "unknown",
|
|
261
|
+
latestVersion: parts[2] ?? "latest",
|
|
262
|
+
type: "package"
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
managerId: this.id,
|
|
270
|
+
managerName: this.name,
|
|
271
|
+
icon: this.icon,
|
|
272
|
+
category: this.category,
|
|
273
|
+
available: true,
|
|
274
|
+
updates,
|
|
275
|
+
durationMs: Date.now() - startTime
|
|
276
|
+
};
|
|
277
|
+
} catch (err) {
|
|
278
|
+
return {
|
|
279
|
+
managerId: this.id,
|
|
280
|
+
managerName: this.name,
|
|
281
|
+
icon: this.icon,
|
|
282
|
+
category: this.category,
|
|
283
|
+
available: true,
|
|
284
|
+
updates: [],
|
|
285
|
+
durationMs: Date.now() - startTime,
|
|
286
|
+
error: err instanceof Error ? err.message : String(err)
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async executeUpdate(items, options) {
|
|
291
|
+
const startTime = Date.now();
|
|
292
|
+
const steps = [];
|
|
293
|
+
if (items.length === 0) {
|
|
294
|
+
return {
|
|
295
|
+
managerId: this.id,
|
|
296
|
+
managerName: this.name,
|
|
297
|
+
icon: this.icon,
|
|
298
|
+
success: true,
|
|
299
|
+
updatedCount: 0,
|
|
300
|
+
durationMs: Date.now() - startTime,
|
|
301
|
+
steps: []
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
const updateStep = await this.executeStep(
|
|
305
|
+
"brew update",
|
|
306
|
+
"brew",
|
|
307
|
+
["update"],
|
|
308
|
+
options
|
|
309
|
+
);
|
|
310
|
+
steps.push(updateStep);
|
|
311
|
+
const hasFormulae = items.some((i) => i.type === "formula" || !i.type || i.type === "package");
|
|
312
|
+
const hasCasks = items.some((i) => i.type === "cask");
|
|
313
|
+
let upgradeSucceeded = false;
|
|
314
|
+
if (hasCasks && !hasFormulae) {
|
|
315
|
+
const caskStep = await this.executeStep(
|
|
316
|
+
"brew upgrade --cask",
|
|
317
|
+
"brew",
|
|
318
|
+
["upgrade", "--cask"],
|
|
319
|
+
options
|
|
320
|
+
);
|
|
321
|
+
steps.push(caskStep);
|
|
322
|
+
upgradeSucceeded = caskStep.status === "success";
|
|
323
|
+
} else if (hasFormulae && !hasCasks) {
|
|
324
|
+
const formulaStep = await this.executeStep(
|
|
325
|
+
"brew upgrade --formula",
|
|
326
|
+
"brew",
|
|
327
|
+
["upgrade", "--formula"],
|
|
328
|
+
options
|
|
329
|
+
);
|
|
330
|
+
steps.push(formulaStep);
|
|
331
|
+
upgradeSucceeded = formulaStep.status === "success";
|
|
332
|
+
} else {
|
|
333
|
+
const upgradeStep = await this.executeStep(
|
|
334
|
+
"brew upgrade",
|
|
335
|
+
"brew",
|
|
336
|
+
["upgrade"],
|
|
337
|
+
options
|
|
338
|
+
);
|
|
339
|
+
steps.push(upgradeStep);
|
|
340
|
+
upgradeSucceeded = upgradeStep.status === "success";
|
|
341
|
+
}
|
|
342
|
+
const cleanupStep = await this.executeStep(
|
|
343
|
+
"brew cleanup",
|
|
344
|
+
"brew",
|
|
345
|
+
["cleanup"],
|
|
346
|
+
options
|
|
347
|
+
);
|
|
348
|
+
steps.push(cleanupStep);
|
|
349
|
+
const failedSteps = steps.filter((s) => s.status === "failed");
|
|
350
|
+
const success = failedSteps.length === 0;
|
|
351
|
+
let error;
|
|
352
|
+
if (failedSteps.length > 0) {
|
|
353
|
+
const failedDetails = failedSteps.map((s) => `${s.name}${s.error ? `: ${s.error}` : ""}`).join("; ");
|
|
354
|
+
error = `${failedSteps.length} step(s) failed: ${failedDetails}`;
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
managerId: this.id,
|
|
358
|
+
managerName: this.name,
|
|
359
|
+
icon: this.icon,
|
|
360
|
+
success,
|
|
361
|
+
updatedCount: upgradeSucceeded ? items.length : 0,
|
|
362
|
+
durationMs: Date.now() - startTime,
|
|
363
|
+
steps,
|
|
364
|
+
error
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
// src/managers/npm.ts
|
|
370
|
+
var NpmManager = class extends BasePackageManager {
|
|
371
|
+
id = "npm";
|
|
372
|
+
name = "npm (global)";
|
|
373
|
+
icon = "\u{1F4E6}";
|
|
374
|
+
category = "runtime";
|
|
375
|
+
binary = "npm";
|
|
376
|
+
async checkUpdates(options) {
|
|
377
|
+
const startTime = Date.now();
|
|
378
|
+
const isAvail = await this.isAvailable();
|
|
379
|
+
if (!isAvail) {
|
|
380
|
+
return {
|
|
381
|
+
managerId: this.id,
|
|
382
|
+
managerName: this.name,
|
|
383
|
+
icon: this.icon,
|
|
384
|
+
category: this.category,
|
|
385
|
+
available: false,
|
|
386
|
+
updates: [],
|
|
387
|
+
durationMs: 0
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
try {
|
|
391
|
+
const execRes = await safeExec("npm", ["outdated", "-g", "--json"], {
|
|
392
|
+
timeoutMs: options?.timeoutMs ?? 25e3
|
|
393
|
+
});
|
|
394
|
+
const updates = [];
|
|
395
|
+
const stdout = execRes.stdout.trim();
|
|
396
|
+
if (stdout.startsWith("{")) {
|
|
397
|
+
const parsed = JSON.parse(stdout);
|
|
398
|
+
for (const [name, info] of Object.entries(parsed)) {
|
|
399
|
+
updates.push({
|
|
400
|
+
managerId: this.id,
|
|
401
|
+
managerName: this.name,
|
|
402
|
+
name,
|
|
403
|
+
currentVersion: info.current ?? "unknown",
|
|
404
|
+
latestVersion: info.latest ?? info.wanted ?? "latest",
|
|
405
|
+
type: "global-pkg"
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return {
|
|
410
|
+
managerId: this.id,
|
|
411
|
+
managerName: this.name,
|
|
412
|
+
icon: this.icon,
|
|
413
|
+
category: this.category,
|
|
414
|
+
available: true,
|
|
415
|
+
updates,
|
|
416
|
+
durationMs: Date.now() - startTime
|
|
417
|
+
};
|
|
418
|
+
} catch (err) {
|
|
419
|
+
return {
|
|
420
|
+
managerId: this.id,
|
|
421
|
+
managerName: this.name,
|
|
422
|
+
icon: this.icon,
|
|
423
|
+
category: this.category,
|
|
424
|
+
available: true,
|
|
425
|
+
updates: [],
|
|
426
|
+
durationMs: Date.now() - startTime,
|
|
427
|
+
error: err instanceof Error ? err.message : String(err)
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
async executeUpdate(items, options) {
|
|
432
|
+
const startTime = Date.now();
|
|
433
|
+
const steps = [];
|
|
434
|
+
let updatedCount = 0;
|
|
435
|
+
if (items.length === 0) {
|
|
436
|
+
return {
|
|
437
|
+
managerId: this.id,
|
|
438
|
+
managerName: this.name,
|
|
439
|
+
icon: this.icon,
|
|
440
|
+
success: true,
|
|
441
|
+
updatedCount: 0,
|
|
442
|
+
durationMs: Date.now() - startTime,
|
|
443
|
+
steps: []
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
let runIsolatedFallback = true;
|
|
447
|
+
if (items.length > 1) {
|
|
448
|
+
const targets = items.map(
|
|
449
|
+
(i) => i.latestVersion && i.latestVersion !== "unknown" ? `${i.name}@${i.latestVersion}` : `${i.name}@latest`
|
|
450
|
+
);
|
|
451
|
+
const batchStep = await this.executeStep(
|
|
452
|
+
`npm install -g --legacy-peer-deps (${items.length} pkgs)`,
|
|
453
|
+
"npm",
|
|
454
|
+
["install", "-g", "--legacy-peer-deps", "--", ...targets],
|
|
455
|
+
options
|
|
456
|
+
);
|
|
457
|
+
if (batchStep.status === "success") {
|
|
458
|
+
steps.push(batchStep);
|
|
459
|
+
updatedCount = items.length;
|
|
460
|
+
runIsolatedFallback = false;
|
|
461
|
+
} else {
|
|
462
|
+
batchStep.status = "skipped";
|
|
463
|
+
steps.push(batchStep);
|
|
464
|
+
options.onStepProgress?.(
|
|
465
|
+
"npm install",
|
|
466
|
+
"Batch install hit conflicts; falling back to per-package isolated installs..."
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (runIsolatedFallback) {
|
|
471
|
+
for (const item of items) {
|
|
472
|
+
const pkgTarget = item.latestVersion && item.latestVersion !== "unknown" ? `${item.name}@${item.latestVersion}` : `${item.name}@latest`;
|
|
473
|
+
const itemStep = await this.executeStep(
|
|
474
|
+
`npm install -g ${item.name}`,
|
|
475
|
+
"npm",
|
|
476
|
+
["install", "-g", "--legacy-peer-deps", "--", pkgTarget],
|
|
477
|
+
options
|
|
478
|
+
);
|
|
479
|
+
if (itemStep.status === "success") {
|
|
480
|
+
updatedCount++;
|
|
481
|
+
steps.push(itemStep);
|
|
482
|
+
} else {
|
|
483
|
+
const errLower = (itemStep.error ?? "").toLowerCase();
|
|
484
|
+
const isScriptIssue = errLower.includes("only-allow") || errLower.includes("sh -c") || errLower.includes("preinstall") || errLower.includes("postinstall") || errLower.includes("install script") || errLower.includes("node-gyp") || errLower.includes("254");
|
|
485
|
+
if (isScriptIssue) {
|
|
486
|
+
options.onStepProgress?.(
|
|
487
|
+
`npm install -g ${item.name}`,
|
|
488
|
+
`Install script failed; retrying with --ignore-scripts...`
|
|
489
|
+
);
|
|
490
|
+
const retryStep = await this.executeStep(
|
|
491
|
+
`npm install -g ${item.name} (--ignore-scripts)`,
|
|
492
|
+
"npm",
|
|
493
|
+
["install", "-g", "--legacy-peer-deps", "--ignore-scripts", "--", pkgTarget],
|
|
494
|
+
options
|
|
495
|
+
);
|
|
496
|
+
if (retryStep.status === "success") {
|
|
497
|
+
updatedCount++;
|
|
498
|
+
steps.push(retryStep);
|
|
499
|
+
} else {
|
|
500
|
+
steps.push(retryStep);
|
|
501
|
+
}
|
|
502
|
+
} else {
|
|
503
|
+
steps.push(itemStep);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
const failedSteps = steps.filter((s) => s.status === "failed");
|
|
509
|
+
const success = failedSteps.length === 0;
|
|
510
|
+
let errorSummary;
|
|
511
|
+
if (!success) {
|
|
512
|
+
const failedDetails = failedSteps.map((s) => `${s.name}${s.error ? `: ${s.error}` : ""}`).join("; ");
|
|
513
|
+
errorSummary = `${failedSteps.length} step(s) failed: ${failedDetails}`;
|
|
514
|
+
}
|
|
515
|
+
return {
|
|
516
|
+
managerId: this.id,
|
|
517
|
+
managerName: this.name,
|
|
518
|
+
icon: this.icon,
|
|
519
|
+
success,
|
|
520
|
+
updatedCount,
|
|
521
|
+
durationMs: Date.now() - startTime,
|
|
522
|
+
steps,
|
|
523
|
+
error: errorSummary
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
// src/managers/pnpm.ts
|
|
529
|
+
var PnpmManager = class extends BasePackageManager {
|
|
530
|
+
id = "pnpm";
|
|
531
|
+
name = "pnpm (global)";
|
|
532
|
+
icon = "\u26A1";
|
|
533
|
+
category = "runtime";
|
|
534
|
+
binary = "pnpm";
|
|
535
|
+
async checkUpdates(options) {
|
|
536
|
+
const startTime = Date.now();
|
|
537
|
+
const isAvail = await this.isAvailable();
|
|
538
|
+
if (!isAvail) {
|
|
539
|
+
return {
|
|
540
|
+
managerId: this.id,
|
|
541
|
+
managerName: this.name,
|
|
542
|
+
icon: this.icon,
|
|
543
|
+
category: this.category,
|
|
544
|
+
available: false,
|
|
545
|
+
updates: [],
|
|
546
|
+
durationMs: 0
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
try {
|
|
550
|
+
const execRes = await safeExec("pnpm", ["outdated", "-g", "--format", "json"], {
|
|
551
|
+
timeoutMs: options?.timeoutMs ?? 25e3
|
|
552
|
+
});
|
|
553
|
+
const updates = [];
|
|
554
|
+
const stdout = execRes.stdout.trim();
|
|
555
|
+
const jsonStart = stdout.indexOf("{");
|
|
556
|
+
const jsonEnd = stdout.lastIndexOf("}");
|
|
557
|
+
if (jsonStart !== -1 && jsonEnd !== -1 && jsonEnd > jsonStart) {
|
|
558
|
+
const jsonStr = stdout.slice(jsonStart, jsonEnd + 1);
|
|
559
|
+
try {
|
|
560
|
+
const parsed = JSON.parse(jsonStr);
|
|
561
|
+
for (const [name, info] of Object.entries(parsed)) {
|
|
562
|
+
updates.push({
|
|
563
|
+
managerId: this.id,
|
|
564
|
+
managerName: this.name,
|
|
565
|
+
name,
|
|
566
|
+
currentVersion: info.current ?? "unknown",
|
|
567
|
+
latestVersion: info.latest ?? info.wanted ?? "latest",
|
|
568
|
+
type: "global-pkg"
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
} catch {
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return {
|
|
575
|
+
managerId: this.id,
|
|
576
|
+
managerName: this.name,
|
|
577
|
+
icon: this.icon,
|
|
578
|
+
category: this.category,
|
|
579
|
+
available: true,
|
|
580
|
+
updates,
|
|
581
|
+
durationMs: Date.now() - startTime
|
|
582
|
+
};
|
|
583
|
+
} catch (err) {
|
|
584
|
+
return {
|
|
585
|
+
managerId: this.id,
|
|
586
|
+
managerName: this.name,
|
|
587
|
+
icon: this.icon,
|
|
588
|
+
category: this.category,
|
|
589
|
+
available: true,
|
|
590
|
+
updates: [],
|
|
591
|
+
durationMs: Date.now() - startTime,
|
|
592
|
+
error: err instanceof Error ? err.message : String(err)
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
async executeUpdate(items, options) {
|
|
597
|
+
const startTime = Date.now();
|
|
598
|
+
const steps = [];
|
|
599
|
+
const updateStep = await this.executeStep(
|
|
600
|
+
"pnpm update -g --latest",
|
|
601
|
+
"pnpm",
|
|
602
|
+
["update", "-g", "--latest"],
|
|
603
|
+
options
|
|
604
|
+
);
|
|
605
|
+
steps.push(updateStep);
|
|
606
|
+
const success = steps.every((s) => s.status === "success");
|
|
607
|
+
return {
|
|
608
|
+
managerId: this.id,
|
|
609
|
+
managerName: this.name,
|
|
610
|
+
icon: this.icon,
|
|
611
|
+
success,
|
|
612
|
+
updatedCount: success ? items.length : 0,
|
|
613
|
+
durationMs: Date.now() - startTime,
|
|
614
|
+
steps,
|
|
615
|
+
error: !success ? steps.find((s) => s.status === "failed")?.error : void 0
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
// src/managers/bun.ts
|
|
621
|
+
var BunManager = class extends BasePackageManager {
|
|
622
|
+
id = "bun";
|
|
623
|
+
name = "Bun";
|
|
624
|
+
icon = "\u{1F35E}";
|
|
625
|
+
category = "runtime";
|
|
626
|
+
binary = "bun";
|
|
627
|
+
async checkUpdates(options) {
|
|
628
|
+
const startTime = Date.now();
|
|
629
|
+
const isAvail = await this.isAvailable();
|
|
630
|
+
if (!isAvail) {
|
|
631
|
+
return {
|
|
632
|
+
managerId: this.id,
|
|
633
|
+
managerName: this.name,
|
|
634
|
+
icon: this.icon,
|
|
635
|
+
category: this.category,
|
|
636
|
+
available: false,
|
|
637
|
+
updates: [],
|
|
638
|
+
durationMs: 0
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
try {
|
|
642
|
+
const execRes = await safeExec("bun", ["outdated", "-g"], {
|
|
643
|
+
timeoutMs: options?.timeoutMs ?? 25e3
|
|
644
|
+
});
|
|
645
|
+
const updates = [];
|
|
646
|
+
const lines = execRes.stdout.split("\n");
|
|
647
|
+
for (const line of lines) {
|
|
648
|
+
const trimmed = line.trim();
|
|
649
|
+
if (!trimmed.startsWith("|") || trimmed.includes("---") || trimmed.includes("Package")) {
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
const cols = trimmed.split("|").map((c) => c.trim()).filter(Boolean);
|
|
653
|
+
if (cols.length >= 3) {
|
|
654
|
+
const pkgName = cols[0];
|
|
655
|
+
const current = cols[1];
|
|
656
|
+
const latest = cols[cols.length - 1];
|
|
657
|
+
if (pkgName && current && latest) {
|
|
658
|
+
updates.push({
|
|
659
|
+
managerId: this.id,
|
|
660
|
+
managerName: this.name,
|
|
661
|
+
name: pkgName,
|
|
662
|
+
currentVersion: current,
|
|
663
|
+
latestVersion: latest,
|
|
664
|
+
type: "global-pkg"
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return {
|
|
670
|
+
managerId: this.id,
|
|
671
|
+
managerName: this.name,
|
|
672
|
+
icon: this.icon,
|
|
673
|
+
category: this.category,
|
|
674
|
+
available: true,
|
|
675
|
+
updates,
|
|
676
|
+
durationMs: Date.now() - startTime
|
|
677
|
+
};
|
|
678
|
+
} catch (err) {
|
|
679
|
+
return {
|
|
680
|
+
managerId: this.id,
|
|
681
|
+
managerName: this.name,
|
|
682
|
+
icon: this.icon,
|
|
683
|
+
category: this.category,
|
|
684
|
+
available: true,
|
|
685
|
+
updates: [],
|
|
686
|
+
durationMs: Date.now() - startTime,
|
|
687
|
+
error: err instanceof Error ? err.message : String(err)
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
async executeUpdate(items, options) {
|
|
692
|
+
const startTime = Date.now();
|
|
693
|
+
const steps = [];
|
|
694
|
+
if (items.length > 0) {
|
|
695
|
+
const pkgsToUpgrade = items.map(
|
|
696
|
+
(item) => item.latestVersion && item.latestVersion !== "latest" && item.latestVersion !== "unknown" ? `${item.name}@${item.latestVersion}` : `${item.name}@latest`
|
|
697
|
+
);
|
|
698
|
+
const addStep = await this.executeStep(
|
|
699
|
+
`bun add -g ${pkgsToUpgrade.length} package(s)`,
|
|
700
|
+
"bun",
|
|
701
|
+
["add", "-g", ...pkgsToUpgrade],
|
|
702
|
+
options
|
|
703
|
+
);
|
|
704
|
+
steps.push(addStep);
|
|
705
|
+
}
|
|
706
|
+
const updateStep = await this.executeStep(
|
|
707
|
+
"bun update -g",
|
|
708
|
+
"bun",
|
|
709
|
+
["update", "-g"],
|
|
710
|
+
options
|
|
711
|
+
);
|
|
712
|
+
steps.push(updateStep);
|
|
713
|
+
const whichBun = await safeExec("which", ["bun"], { timeoutMs: 3e3 });
|
|
714
|
+
const isBrewBun = whichBun.stdout.includes("/opt/homebrew") || whichBun.stdout.includes("/Cellar");
|
|
715
|
+
if (!isBrewBun) {
|
|
716
|
+
const upgradeStep = await this.executeStep(
|
|
717
|
+
"bun upgrade",
|
|
718
|
+
"bun",
|
|
719
|
+
["upgrade"],
|
|
720
|
+
options
|
|
721
|
+
);
|
|
722
|
+
if (upgradeStep.status === "failed" && upgradeStep.error?.includes("Homebrew")) {
|
|
723
|
+
upgradeStep.status = "skipped";
|
|
724
|
+
}
|
|
725
|
+
steps.push(upgradeStep);
|
|
726
|
+
}
|
|
727
|
+
const hasFailedStep = steps.some((s) => s.status === "failed");
|
|
728
|
+
const success = !hasFailedStep;
|
|
729
|
+
return {
|
|
730
|
+
managerId: this.id,
|
|
731
|
+
managerName: this.name,
|
|
732
|
+
icon: this.icon,
|
|
733
|
+
success,
|
|
734
|
+
updatedCount: success ? items.length : 0,
|
|
735
|
+
durationMs: Date.now() - startTime,
|
|
736
|
+
steps,
|
|
737
|
+
error: !success ? steps.find((s) => s.status === "failed")?.error : void 0
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
// src/managers/yarn.ts
|
|
743
|
+
var YarnManager = class extends BasePackageManager {
|
|
744
|
+
id = "yarn";
|
|
745
|
+
name = "Yarn (global)";
|
|
746
|
+
icon = "\u{1F9F6}";
|
|
747
|
+
category = "runtime";
|
|
748
|
+
binary = "yarn";
|
|
749
|
+
async checkUpdates(options) {
|
|
750
|
+
const startTime = Date.now();
|
|
751
|
+
const isAvail = await this.isAvailable();
|
|
752
|
+
if (!isAvail) {
|
|
753
|
+
return {
|
|
754
|
+
managerId: this.id,
|
|
755
|
+
managerName: this.name,
|
|
756
|
+
icon: this.icon,
|
|
757
|
+
category: this.category,
|
|
758
|
+
available: false,
|
|
759
|
+
updates: [],
|
|
760
|
+
durationMs: 0
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
try {
|
|
764
|
+
const execRes = await safeExec("yarn", ["global", "list", "--depth=0"], {
|
|
765
|
+
timeoutMs: options?.timeoutMs ?? 15e3
|
|
766
|
+
});
|
|
767
|
+
const updates = [];
|
|
768
|
+
const lines = execRes.stdout.split("\n");
|
|
769
|
+
const installed = [];
|
|
770
|
+
for (const line of lines) {
|
|
771
|
+
const match = line.match(/info\s+"((?:@[^/]+\/)?[^@]+)@([^"]+)"/);
|
|
772
|
+
if (match) {
|
|
773
|
+
installed.push({ name: match[1], currentVersion: match[2] });
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
await Promise.all(
|
|
777
|
+
installed.map(async ({ name, currentVersion }) => {
|
|
778
|
+
try {
|
|
779
|
+
const res = await fetch(
|
|
780
|
+
`https://registry.npmjs.org/${encodeURIComponent(name).replace("%40", "@")}/latest`,
|
|
781
|
+
{
|
|
782
|
+
signal: AbortSignal.timeout(3e3),
|
|
783
|
+
headers: { Accept: "application/json" }
|
|
784
|
+
}
|
|
785
|
+
);
|
|
786
|
+
if (res.ok) {
|
|
787
|
+
const data = await res.json();
|
|
788
|
+
const latest = data.version;
|
|
789
|
+
if (latest && latest !== currentVersion) {
|
|
790
|
+
updates.push({
|
|
791
|
+
managerId: this.id,
|
|
792
|
+
managerName: this.name,
|
|
793
|
+
name,
|
|
794
|
+
currentVersion,
|
|
795
|
+
latestVersion: latest,
|
|
796
|
+
type: "global-pkg"
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
} catch {
|
|
801
|
+
}
|
|
802
|
+
})
|
|
803
|
+
);
|
|
804
|
+
return {
|
|
805
|
+
managerId: this.id,
|
|
806
|
+
managerName: this.name,
|
|
807
|
+
icon: this.icon,
|
|
808
|
+
category: this.category,
|
|
809
|
+
available: true,
|
|
810
|
+
updates,
|
|
811
|
+
durationMs: Date.now() - startTime
|
|
812
|
+
};
|
|
813
|
+
} catch (err) {
|
|
814
|
+
return {
|
|
815
|
+
managerId: this.id,
|
|
816
|
+
managerName: this.name,
|
|
817
|
+
icon: this.icon,
|
|
818
|
+
category: this.category,
|
|
819
|
+
available: true,
|
|
820
|
+
updates: [],
|
|
821
|
+
durationMs: Date.now() - startTime,
|
|
822
|
+
error: err instanceof Error ? err.message : String(err)
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
async executeUpdate(items, options) {
|
|
827
|
+
const startTime = Date.now();
|
|
828
|
+
const steps = [];
|
|
829
|
+
if (items.length > 0) {
|
|
830
|
+
const pkgsToUpgrade = items.map(
|
|
831
|
+
(item) => item.latestVersion && item.latestVersion !== "latest" && item.latestVersion !== "unknown" ? `${item.name}@${item.latestVersion}` : `${item.name}@latest`
|
|
832
|
+
);
|
|
833
|
+
const addStep = await this.executeStep(
|
|
834
|
+
`yarn global add ${pkgsToUpgrade.length} package(s)`,
|
|
835
|
+
"yarn",
|
|
836
|
+
["global", "add", ...pkgsToUpgrade],
|
|
837
|
+
options
|
|
838
|
+
);
|
|
839
|
+
steps.push(addStep);
|
|
840
|
+
}
|
|
841
|
+
const upgradeStep = await this.executeStep(
|
|
842
|
+
"yarn global upgrade",
|
|
843
|
+
"yarn",
|
|
844
|
+
["global", "upgrade"],
|
|
845
|
+
options
|
|
846
|
+
);
|
|
847
|
+
steps.push(upgradeStep);
|
|
848
|
+
const success = steps.every((s) => s.status === "success");
|
|
849
|
+
return {
|
|
850
|
+
managerId: this.id,
|
|
851
|
+
managerName: this.name,
|
|
852
|
+
icon: this.icon,
|
|
853
|
+
success,
|
|
854
|
+
updatedCount: success ? items.length : 0,
|
|
855
|
+
durationMs: Date.now() - startTime,
|
|
856
|
+
steps,
|
|
857
|
+
error: !success ? steps.find((s) => s.status === "failed")?.error : void 0
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
// src/managers/python.ts
|
|
863
|
+
var PythonPipManager = class extends BasePackageManager {
|
|
864
|
+
id = "pip";
|
|
865
|
+
name = "Python (pip user)";
|
|
866
|
+
icon = "\u{1F40D}";
|
|
867
|
+
category = "language";
|
|
868
|
+
binary = "pip3";
|
|
869
|
+
activeBinary = "pip3";
|
|
870
|
+
async isAvailable() {
|
|
871
|
+
if (await commandExists("pip3")) {
|
|
872
|
+
this.activeBinary = "pip3";
|
|
873
|
+
return true;
|
|
874
|
+
}
|
|
875
|
+
if (await commandExists("pip")) {
|
|
876
|
+
this.activeBinary = "pip";
|
|
877
|
+
return true;
|
|
878
|
+
}
|
|
879
|
+
return false;
|
|
880
|
+
}
|
|
881
|
+
async checkUpdates(options) {
|
|
882
|
+
const startTime = Date.now();
|
|
883
|
+
const isAvail = await this.isAvailable();
|
|
884
|
+
if (!isAvail) {
|
|
885
|
+
return {
|
|
886
|
+
managerId: this.id,
|
|
887
|
+
managerName: this.name,
|
|
888
|
+
icon: this.icon,
|
|
889
|
+
category: this.category,
|
|
890
|
+
available: false,
|
|
891
|
+
updates: [],
|
|
892
|
+
durationMs: 0
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
try {
|
|
896
|
+
const execRes = await safeExec(
|
|
897
|
+
this.activeBinary,
|
|
898
|
+
["list", "--user", "--outdated", "--format=json"],
|
|
899
|
+
{ timeoutMs: options?.timeoutMs ?? 15e3 }
|
|
900
|
+
);
|
|
901
|
+
const updates = [];
|
|
902
|
+
const stdout = execRes.stdout.trim();
|
|
903
|
+
if (stdout.startsWith("[")) {
|
|
904
|
+
try {
|
|
905
|
+
const parsed = JSON.parse(stdout);
|
|
906
|
+
for (const item of parsed) {
|
|
907
|
+
updates.push({
|
|
908
|
+
managerId: this.id,
|
|
909
|
+
managerName: this.name,
|
|
910
|
+
name: item.name,
|
|
911
|
+
currentVersion: item.version ?? "unknown",
|
|
912
|
+
latestVersion: item.latest_version ?? "latest",
|
|
913
|
+
type: "pip-user"
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
} catch {
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
return {
|
|
920
|
+
managerId: this.id,
|
|
921
|
+
managerName: this.name,
|
|
922
|
+
icon: this.icon,
|
|
923
|
+
category: this.category,
|
|
924
|
+
available: true,
|
|
925
|
+
updates,
|
|
926
|
+
durationMs: Date.now() - startTime
|
|
927
|
+
};
|
|
928
|
+
} catch (err) {
|
|
929
|
+
return {
|
|
930
|
+
managerId: this.id,
|
|
931
|
+
managerName: this.name,
|
|
932
|
+
icon: this.icon,
|
|
933
|
+
category: this.category,
|
|
934
|
+
available: true,
|
|
935
|
+
updates: [],
|
|
936
|
+
durationMs: Date.now() - startTime,
|
|
937
|
+
error: err instanceof Error ? err.message : String(err)
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
async executeUpdate(items, options) {
|
|
942
|
+
const startTime = Date.now();
|
|
943
|
+
const steps = [];
|
|
944
|
+
if (items.length > 0) {
|
|
945
|
+
const packageNames = items.map((i) => i.name);
|
|
946
|
+
const step = await this.executeStep(
|
|
947
|
+
`${this.activeBinary} install --user --upgrade (${items.length} pkgs)`,
|
|
948
|
+
this.activeBinary,
|
|
949
|
+
["install", "--user", "--upgrade", ...packageNames],
|
|
950
|
+
options
|
|
951
|
+
);
|
|
952
|
+
steps.push(step);
|
|
953
|
+
}
|
|
954
|
+
const success = steps.every((s) => s.status === "success");
|
|
955
|
+
return {
|
|
956
|
+
managerId: this.id,
|
|
957
|
+
managerName: this.name,
|
|
958
|
+
icon: this.icon,
|
|
959
|
+
success,
|
|
960
|
+
updatedCount: success ? items.length : 0,
|
|
961
|
+
durationMs: Date.now() - startTime,
|
|
962
|
+
steps,
|
|
963
|
+
error: !success ? steps.find((s) => s.status === "failed")?.error : void 0
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
|
|
968
|
+
// src/managers/pipx.ts
|
|
969
|
+
var PipxManager = class extends BasePackageManager {
|
|
970
|
+
id = "pipx";
|
|
971
|
+
name = "pipx (Python CLI tools)";
|
|
972
|
+
icon = "\u{1F4E6}";
|
|
973
|
+
category = "language";
|
|
974
|
+
binary = "pipx";
|
|
975
|
+
async checkUpdates(options) {
|
|
976
|
+
const startTime = Date.now();
|
|
977
|
+
const isAvail = await this.isAvailable();
|
|
978
|
+
if (!isAvail) {
|
|
979
|
+
return {
|
|
980
|
+
managerId: this.id,
|
|
981
|
+
managerName: this.name,
|
|
982
|
+
icon: this.icon,
|
|
983
|
+
category: this.category,
|
|
984
|
+
available: false,
|
|
985
|
+
updates: [],
|
|
986
|
+
durationMs: 0
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
try {
|
|
990
|
+
const execRes = await safeExec("pipx", ["list", "--json"], {
|
|
991
|
+
timeoutMs: options?.timeoutMs ?? 15e3
|
|
992
|
+
});
|
|
993
|
+
const updates = [];
|
|
994
|
+
const stdout = execRes.stdout.trim();
|
|
995
|
+
if (stdout.startsWith("{")) {
|
|
996
|
+
try {
|
|
997
|
+
const parsed = JSON.parse(stdout);
|
|
998
|
+
if (parsed.venvs) {
|
|
999
|
+
const entries = Object.entries(parsed.venvs);
|
|
1000
|
+
await Promise.all(
|
|
1001
|
+
entries.map(async ([pkgName, venvData]) => {
|
|
1002
|
+
const current = venvData.metadata?.main_package?.package_version;
|
|
1003
|
+
if (!current) return;
|
|
1004
|
+
try {
|
|
1005
|
+
const response = await fetch(
|
|
1006
|
+
`https://pypi.org/pypi/${encodeURIComponent(pkgName)}/json`,
|
|
1007
|
+
{ signal: AbortSignal.timeout(3e3) }
|
|
1008
|
+
);
|
|
1009
|
+
if (response.ok) {
|
|
1010
|
+
const data = await response.json();
|
|
1011
|
+
const latest = data.info?.version;
|
|
1012
|
+
if (latest && latest !== current) {
|
|
1013
|
+
updates.push({
|
|
1014
|
+
managerId: this.id,
|
|
1015
|
+
managerName: this.name,
|
|
1016
|
+
name: pkgName,
|
|
1017
|
+
currentVersion: current,
|
|
1018
|
+
latestVersion: latest,
|
|
1019
|
+
type: "pipx-app"
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
} catch {
|
|
1024
|
+
}
|
|
1025
|
+
})
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
} catch {
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
return {
|
|
1032
|
+
managerId: this.id,
|
|
1033
|
+
managerName: this.name,
|
|
1034
|
+
icon: this.icon,
|
|
1035
|
+
category: this.category,
|
|
1036
|
+
available: true,
|
|
1037
|
+
updates,
|
|
1038
|
+
durationMs: Date.now() - startTime
|
|
1039
|
+
};
|
|
1040
|
+
} catch (err) {
|
|
1041
|
+
return {
|
|
1042
|
+
managerId: this.id,
|
|
1043
|
+
managerName: this.name,
|
|
1044
|
+
icon: this.icon,
|
|
1045
|
+
category: this.category,
|
|
1046
|
+
available: true,
|
|
1047
|
+
updates: [],
|
|
1048
|
+
durationMs: Date.now() - startTime,
|
|
1049
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
async executeUpdate(items, options) {
|
|
1054
|
+
const startTime = Date.now();
|
|
1055
|
+
const steps = [];
|
|
1056
|
+
const upgradeStep = await this.executeStep(
|
|
1057
|
+
"pipx upgrade-all",
|
|
1058
|
+
"pipx",
|
|
1059
|
+
["upgrade-all"],
|
|
1060
|
+
options
|
|
1061
|
+
);
|
|
1062
|
+
steps.push(upgradeStep);
|
|
1063
|
+
const success = steps.every((s) => s.status === "success");
|
|
1064
|
+
return {
|
|
1065
|
+
managerId: this.id,
|
|
1066
|
+
managerName: this.name,
|
|
1067
|
+
icon: this.icon,
|
|
1068
|
+
success,
|
|
1069
|
+
updatedCount: success ? items.length : 0,
|
|
1070
|
+
durationMs: Date.now() - startTime,
|
|
1071
|
+
steps,
|
|
1072
|
+
error: !success ? steps.find((s) => s.status === "failed")?.error : void 0
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
|
|
1077
|
+
// src/managers/cargo.ts
|
|
1078
|
+
var CargoManager = class extends BasePackageManager {
|
|
1079
|
+
id = "cargo";
|
|
1080
|
+
name = "Rust (Cargo & Rustup)";
|
|
1081
|
+
icon = "\u{1F980}";
|
|
1082
|
+
category = "language";
|
|
1083
|
+
binary = "cargo";
|
|
1084
|
+
async isAvailable() {
|
|
1085
|
+
return await commandExists("cargo") || await commandExists("rustup");
|
|
1086
|
+
}
|
|
1087
|
+
async checkUpdates(options) {
|
|
1088
|
+
const startTime = Date.now();
|
|
1089
|
+
const isAvail = await this.isAvailable();
|
|
1090
|
+
if (!isAvail) {
|
|
1091
|
+
return {
|
|
1092
|
+
managerId: this.id,
|
|
1093
|
+
managerName: this.name,
|
|
1094
|
+
icon: this.icon,
|
|
1095
|
+
category: this.category,
|
|
1096
|
+
available: false,
|
|
1097
|
+
updates: [],
|
|
1098
|
+
durationMs: 0
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
try {
|
|
1102
|
+
const updates = [];
|
|
1103
|
+
if (await commandExists("rustup")) {
|
|
1104
|
+
const checkRes = await safeExec("rustup", ["check"], {
|
|
1105
|
+
timeoutMs: options?.timeoutMs ?? 15e3
|
|
1106
|
+
});
|
|
1107
|
+
const lines = checkRes.stdout.split("\n");
|
|
1108
|
+
for (const line of lines) {
|
|
1109
|
+
if (line.includes("Update available")) {
|
|
1110
|
+
const parts = line.trim().split(/\s+/);
|
|
1111
|
+
const toolchain = parts[0] ?? "toolchain";
|
|
1112
|
+
updates.push({
|
|
1113
|
+
managerId: this.id,
|
|
1114
|
+
managerName: this.name,
|
|
1115
|
+
name: `rustup (${toolchain})`,
|
|
1116
|
+
currentVersion: "installed",
|
|
1117
|
+
latestVersion: "update-available",
|
|
1118
|
+
type: "toolchain"
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
if (await commandExists("cargo-install-update")) {
|
|
1124
|
+
const listRes = await safeExec("cargo", ["install-update", "-l"], {
|
|
1125
|
+
timeoutMs: options?.timeoutMs ?? 15e3
|
|
1126
|
+
});
|
|
1127
|
+
const lines = listRes.stdout.split("\n");
|
|
1128
|
+
for (const line of lines) {
|
|
1129
|
+
const match = line.match(/^(\S+)\s+v(\S+)\s+v(\S+)/);
|
|
1130
|
+
if (match) {
|
|
1131
|
+
const [, name, cur, latest] = match;
|
|
1132
|
+
if (cur !== latest) {
|
|
1133
|
+
updates.push({
|
|
1134
|
+
managerId: this.id,
|
|
1135
|
+
managerName: this.name,
|
|
1136
|
+
name,
|
|
1137
|
+
currentVersion: cur,
|
|
1138
|
+
latestVersion: latest,
|
|
1139
|
+
type: "crate"
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
return {
|
|
1146
|
+
managerId: this.id,
|
|
1147
|
+
managerName: this.name,
|
|
1148
|
+
icon: this.icon,
|
|
1149
|
+
category: this.category,
|
|
1150
|
+
available: true,
|
|
1151
|
+
updates,
|
|
1152
|
+
durationMs: Date.now() - startTime
|
|
1153
|
+
};
|
|
1154
|
+
} catch (err) {
|
|
1155
|
+
return {
|
|
1156
|
+
managerId: this.id,
|
|
1157
|
+
managerName: this.name,
|
|
1158
|
+
icon: this.icon,
|
|
1159
|
+
category: this.category,
|
|
1160
|
+
available: true,
|
|
1161
|
+
updates: [],
|
|
1162
|
+
durationMs: Date.now() - startTime,
|
|
1163
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
async executeUpdate(items, options) {
|
|
1168
|
+
const startTime = Date.now();
|
|
1169
|
+
const steps = [];
|
|
1170
|
+
if (await commandExists("rustup")) {
|
|
1171
|
+
const rustupStep = await this.executeStep(
|
|
1172
|
+
"rustup update",
|
|
1173
|
+
"rustup",
|
|
1174
|
+
["update"],
|
|
1175
|
+
options
|
|
1176
|
+
);
|
|
1177
|
+
steps.push(rustupStep);
|
|
1178
|
+
}
|
|
1179
|
+
if (await commandExists("cargo-install-update")) {
|
|
1180
|
+
const cargoStep = await this.executeStep(
|
|
1181
|
+
"cargo install-update -a",
|
|
1182
|
+
"cargo",
|
|
1183
|
+
["install-update", "-a"],
|
|
1184
|
+
options
|
|
1185
|
+
);
|
|
1186
|
+
steps.push(cargoStep);
|
|
1187
|
+
}
|
|
1188
|
+
const hasFailedStep = steps.some((s) => s.status === "failed");
|
|
1189
|
+
const success = !hasFailedStep;
|
|
1190
|
+
return {
|
|
1191
|
+
managerId: this.id,
|
|
1192
|
+
managerName: this.name,
|
|
1193
|
+
icon: this.icon,
|
|
1194
|
+
success,
|
|
1195
|
+
updatedCount: success ? items.length : 0,
|
|
1196
|
+
durationMs: Date.now() - startTime,
|
|
1197
|
+
steps,
|
|
1198
|
+
error: !success ? steps.find((s) => s.status === "failed")?.error : void 0
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
};
|
|
1202
|
+
|
|
1203
|
+
// src/managers/gem.ts
|
|
1204
|
+
var RubyGemManager = class extends BasePackageManager {
|
|
1205
|
+
id = "gem";
|
|
1206
|
+
name = "Ruby Gem";
|
|
1207
|
+
icon = "\u{1F48E}";
|
|
1208
|
+
category = "language";
|
|
1209
|
+
binary = "gem";
|
|
1210
|
+
async isAvailable() {
|
|
1211
|
+
const hasCmd = await commandExists(this.binary);
|
|
1212
|
+
if (!hasCmd) return false;
|
|
1213
|
+
const whichRes = await safeExec("which", ["gem"], { timeoutMs: 3e3 });
|
|
1214
|
+
const gemPath = whichRes.stdout.trim();
|
|
1215
|
+
if (gemPath === "/usr/bin/gem" || gemPath.startsWith("/System/Library")) {
|
|
1216
|
+
return false;
|
|
1217
|
+
}
|
|
1218
|
+
return true;
|
|
1219
|
+
}
|
|
1220
|
+
async checkUpdates(options) {
|
|
1221
|
+
const startTime = Date.now();
|
|
1222
|
+
const isAvail = await this.isAvailable();
|
|
1223
|
+
if (!isAvail) {
|
|
1224
|
+
return {
|
|
1225
|
+
managerId: this.id,
|
|
1226
|
+
managerName: this.name,
|
|
1227
|
+
icon: this.icon,
|
|
1228
|
+
category: this.category,
|
|
1229
|
+
available: false,
|
|
1230
|
+
updates: [],
|
|
1231
|
+
durationMs: 0
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
try {
|
|
1235
|
+
const execRes = await safeExec("gem", ["outdated"], {
|
|
1236
|
+
timeoutMs: options?.timeoutMs ?? 2e4
|
|
1237
|
+
});
|
|
1238
|
+
const updates = [];
|
|
1239
|
+
const lines = execRes.stdout.split("\n");
|
|
1240
|
+
for (const line of lines) {
|
|
1241
|
+
const trimmed = line.trim();
|
|
1242
|
+
const match = trimmed.match(/^(\S+)\s*\(([^<]+)<\s*([^)]+)\)/);
|
|
1243
|
+
if (match) {
|
|
1244
|
+
const [, name, current, latest] = match;
|
|
1245
|
+
updates.push({
|
|
1246
|
+
managerId: this.id,
|
|
1247
|
+
managerName: this.name,
|
|
1248
|
+
name: name.trim(),
|
|
1249
|
+
currentVersion: current.trim(),
|
|
1250
|
+
latestVersion: latest.trim(),
|
|
1251
|
+
type: "gem"
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
return {
|
|
1256
|
+
managerId: this.id,
|
|
1257
|
+
managerName: this.name,
|
|
1258
|
+
icon: this.icon,
|
|
1259
|
+
category: this.category,
|
|
1260
|
+
available: true,
|
|
1261
|
+
updates,
|
|
1262
|
+
durationMs: Date.now() - startTime
|
|
1263
|
+
};
|
|
1264
|
+
} catch (err) {
|
|
1265
|
+
return {
|
|
1266
|
+
managerId: this.id,
|
|
1267
|
+
managerName: this.name,
|
|
1268
|
+
icon: this.icon,
|
|
1269
|
+
category: this.category,
|
|
1270
|
+
available: true,
|
|
1271
|
+
updates: [],
|
|
1272
|
+
durationMs: Date.now() - startTime,
|
|
1273
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
async executeUpdate(items, options) {
|
|
1278
|
+
const startTime = Date.now();
|
|
1279
|
+
const steps = [];
|
|
1280
|
+
const updateStep = await this.executeStep(
|
|
1281
|
+
"gem update --user-install",
|
|
1282
|
+
"gem",
|
|
1283
|
+
["update", "--user-install"],
|
|
1284
|
+
options
|
|
1285
|
+
);
|
|
1286
|
+
steps.push(updateStep);
|
|
1287
|
+
const cleanupStep = await this.executeStep(
|
|
1288
|
+
"gem cleanup",
|
|
1289
|
+
"gem",
|
|
1290
|
+
["cleanup"],
|
|
1291
|
+
options
|
|
1292
|
+
);
|
|
1293
|
+
steps.push(cleanupStep);
|
|
1294
|
+
const success = steps.every((s) => s.status === "success");
|
|
1295
|
+
return {
|
|
1296
|
+
managerId: this.id,
|
|
1297
|
+
managerName: this.name,
|
|
1298
|
+
icon: this.icon,
|
|
1299
|
+
success,
|
|
1300
|
+
updatedCount: success ? items.length : 0,
|
|
1301
|
+
durationMs: Date.now() - startTime,
|
|
1302
|
+
steps,
|
|
1303
|
+
error: !success ? steps.find((s) => s.status === "failed")?.error : void 0
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
};
|
|
1307
|
+
|
|
1308
|
+
// src/managers/macports.ts
|
|
1309
|
+
var MacPortsManager = class extends BasePackageManager {
|
|
1310
|
+
id = "macports";
|
|
1311
|
+
name = "MacPorts";
|
|
1312
|
+
icon = "\u2693";
|
|
1313
|
+
category = "system";
|
|
1314
|
+
binary = "port";
|
|
1315
|
+
async checkUpdates(options) {
|
|
1316
|
+
const startTime = Date.now();
|
|
1317
|
+
const isAvail = await this.isAvailable();
|
|
1318
|
+
if (!isAvail) {
|
|
1319
|
+
return {
|
|
1320
|
+
managerId: this.id,
|
|
1321
|
+
managerName: this.name,
|
|
1322
|
+
icon: this.icon,
|
|
1323
|
+
category: this.category,
|
|
1324
|
+
available: false,
|
|
1325
|
+
updates: [],
|
|
1326
|
+
durationMs: 0
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
try {
|
|
1330
|
+
const execRes = await safeExec("port", ["outdated"], {
|
|
1331
|
+
timeoutMs: options?.timeoutMs ?? 2e4
|
|
1332
|
+
});
|
|
1333
|
+
const updates = [];
|
|
1334
|
+
const lines = execRes.stdout.split("\n");
|
|
1335
|
+
for (const line of lines) {
|
|
1336
|
+
const trimmed = line.trim();
|
|
1337
|
+
if (!trimmed || trimmed.startsWith("The following") || trimmed.startsWith("--")) {
|
|
1338
|
+
continue;
|
|
1339
|
+
}
|
|
1340
|
+
const match = trimmed.match(/^(\S+)\s+(\S+)\s*<\s*(\S+)/);
|
|
1341
|
+
if (match) {
|
|
1342
|
+
const [, name, current, latest] = match;
|
|
1343
|
+
updates.push({
|
|
1344
|
+
managerId: this.id,
|
|
1345
|
+
managerName: this.name,
|
|
1346
|
+
name,
|
|
1347
|
+
currentVersion: current,
|
|
1348
|
+
latestVersion: latest,
|
|
1349
|
+
type: "port"
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
return {
|
|
1354
|
+
managerId: this.id,
|
|
1355
|
+
managerName: this.name,
|
|
1356
|
+
icon: this.icon,
|
|
1357
|
+
category: this.category,
|
|
1358
|
+
available: true,
|
|
1359
|
+
updates,
|
|
1360
|
+
durationMs: Date.now() - startTime
|
|
1361
|
+
};
|
|
1362
|
+
} catch (err) {
|
|
1363
|
+
return {
|
|
1364
|
+
managerId: this.id,
|
|
1365
|
+
managerName: this.name,
|
|
1366
|
+
icon: this.icon,
|
|
1367
|
+
category: this.category,
|
|
1368
|
+
available: true,
|
|
1369
|
+
updates: [],
|
|
1370
|
+
durationMs: Date.now() - startTime,
|
|
1371
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
async executeUpdate(items, options) {
|
|
1376
|
+
const startTime = Date.now();
|
|
1377
|
+
const steps = [];
|
|
1378
|
+
if (!options.dryRun && process.getuid?.() !== 0) {
|
|
1379
|
+
const sudoCheck = await safeExec("sudo", ["-n", "true"], { timeoutMs: 3e3 });
|
|
1380
|
+
if (!sudoCheck.success) {
|
|
1381
|
+
return {
|
|
1382
|
+
managerId: this.id,
|
|
1383
|
+
managerName: this.name,
|
|
1384
|
+
icon: this.icon,
|
|
1385
|
+
success: false,
|
|
1386
|
+
updatedCount: 0,
|
|
1387
|
+
durationMs: Date.now() - startTime,
|
|
1388
|
+
steps: [
|
|
1389
|
+
{
|
|
1390
|
+
name: "sudo privileges",
|
|
1391
|
+
command: "sudo -n true",
|
|
1392
|
+
status: "failed",
|
|
1393
|
+
error: 'MacPorts requires sudo privileges. Run "sudo -v" in your terminal before hmd.'
|
|
1394
|
+
}
|
|
1395
|
+
],
|
|
1396
|
+
error: 'MacPorts requires sudo privileges. Run "sudo -v" before running hmd.'
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
const selfupdateStep = await this.executeStep(
|
|
1401
|
+
"sudo port selfupdate",
|
|
1402
|
+
"sudo",
|
|
1403
|
+
["port", "selfupdate"],
|
|
1404
|
+
options
|
|
1405
|
+
);
|
|
1406
|
+
steps.push(selfupdateStep);
|
|
1407
|
+
const upgradeStep = await this.executeStep(
|
|
1408
|
+
"sudo port upgrade outdated",
|
|
1409
|
+
"sudo",
|
|
1410
|
+
["port", "upgrade", "outdated"],
|
|
1411
|
+
options
|
|
1412
|
+
);
|
|
1413
|
+
steps.push(upgradeStep);
|
|
1414
|
+
const success = steps.every((s) => s.status === "success");
|
|
1415
|
+
return {
|
|
1416
|
+
managerId: this.id,
|
|
1417
|
+
managerName: this.name,
|
|
1418
|
+
icon: this.icon,
|
|
1419
|
+
success,
|
|
1420
|
+
updatedCount: success ? items.length : 0,
|
|
1421
|
+
durationMs: Date.now() - startTime,
|
|
1422
|
+
steps,
|
|
1423
|
+
error: !success ? steps.find((s) => s.status === "failed")?.error : void 0
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
};
|
|
1427
|
+
|
|
1428
|
+
// src/managers/mas.ts
|
|
1429
|
+
var MacAppStoreManager = class extends BasePackageManager {
|
|
1430
|
+
id = "mas";
|
|
1431
|
+
name = "Mac App Store";
|
|
1432
|
+
icon = "\u{1F34E}";
|
|
1433
|
+
category = "appstore";
|
|
1434
|
+
binary = "mas";
|
|
1435
|
+
async checkUpdates(options) {
|
|
1436
|
+
const startTime = Date.now();
|
|
1437
|
+
const isAvail = await this.isAvailable();
|
|
1438
|
+
if (!isAvail) {
|
|
1439
|
+
return {
|
|
1440
|
+
managerId: this.id,
|
|
1441
|
+
managerName: this.name,
|
|
1442
|
+
icon: this.icon,
|
|
1443
|
+
category: this.category,
|
|
1444
|
+
available: false,
|
|
1445
|
+
updates: [],
|
|
1446
|
+
durationMs: 0
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
try {
|
|
1450
|
+
const execRes = await safeExec("mas", ["outdated"], {
|
|
1451
|
+
timeoutMs: options?.timeoutMs ?? 2e4
|
|
1452
|
+
});
|
|
1453
|
+
const updates = [];
|
|
1454
|
+
const lines = execRes.stdout.split("\n");
|
|
1455
|
+
for (const line of lines) {
|
|
1456
|
+
const trimmed = line.trim();
|
|
1457
|
+
const match = trimmed.match(/^\s*(\d+)\s+(.+?)\s*\(([^->]+)->\s*([^)]+)\)/);
|
|
1458
|
+
if (match) {
|
|
1459
|
+
const [, id, appName, current, latest] = match;
|
|
1460
|
+
updates.push({
|
|
1461
|
+
managerId: this.id,
|
|
1462
|
+
managerName: this.name,
|
|
1463
|
+
name: `${appName.trim()} [${id}]`,
|
|
1464
|
+
currentVersion: current.trim(),
|
|
1465
|
+
latestVersion: latest.trim(),
|
|
1466
|
+
type: "mas-app"
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
return {
|
|
1471
|
+
managerId: this.id,
|
|
1472
|
+
managerName: this.name,
|
|
1473
|
+
icon: this.icon,
|
|
1474
|
+
category: this.category,
|
|
1475
|
+
available: true,
|
|
1476
|
+
updates,
|
|
1477
|
+
durationMs: Date.now() - startTime
|
|
1478
|
+
};
|
|
1479
|
+
} catch (err) {
|
|
1480
|
+
return {
|
|
1481
|
+
managerId: this.id,
|
|
1482
|
+
managerName: this.name,
|
|
1483
|
+
icon: this.icon,
|
|
1484
|
+
category: this.category,
|
|
1485
|
+
available: true,
|
|
1486
|
+
updates: [],
|
|
1487
|
+
durationMs: Date.now() - startTime,
|
|
1488
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
async executeUpdate(items, options) {
|
|
1493
|
+
const startTime = Date.now();
|
|
1494
|
+
const steps = [];
|
|
1495
|
+
const upgradeStep = await this.executeStep(
|
|
1496
|
+
"mas upgrade",
|
|
1497
|
+
"mas",
|
|
1498
|
+
["upgrade"],
|
|
1499
|
+
options
|
|
1500
|
+
);
|
|
1501
|
+
steps.push(upgradeStep);
|
|
1502
|
+
const success = steps.every((s) => s.status === "success");
|
|
1503
|
+
return {
|
|
1504
|
+
managerId: this.id,
|
|
1505
|
+
managerName: this.name,
|
|
1506
|
+
icon: this.icon,
|
|
1507
|
+
success,
|
|
1508
|
+
updatedCount: success ? items.length : 0,
|
|
1509
|
+
durationMs: Date.now() - startTime,
|
|
1510
|
+
steps,
|
|
1511
|
+
error: !success ? steps.find((s) => s.status === "failed")?.error : void 0
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
};
|
|
1515
|
+
|
|
1516
|
+
// src/managers/linux.ts
|
|
1517
|
+
var AptManager = class extends BasePackageManager {
|
|
1518
|
+
id = "apt";
|
|
1519
|
+
name = "APT (Debian/Ubuntu)";
|
|
1520
|
+
icon = "\u{1F427}";
|
|
1521
|
+
category = "system";
|
|
1522
|
+
binary = "apt-get";
|
|
1523
|
+
async checkUpdates(options) {
|
|
1524
|
+
const startTime = Date.now();
|
|
1525
|
+
const isAvail = await this.isAvailable();
|
|
1526
|
+
if (!isAvail) {
|
|
1527
|
+
return {
|
|
1528
|
+
managerId: this.id,
|
|
1529
|
+
managerName: this.name,
|
|
1530
|
+
icon: this.icon,
|
|
1531
|
+
category: this.category,
|
|
1532
|
+
available: false,
|
|
1533
|
+
updates: [],
|
|
1534
|
+
durationMs: 0
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1537
|
+
try {
|
|
1538
|
+
const execRes = await safeExec("apt", ["list", "--upgradable"], {
|
|
1539
|
+
timeoutMs: options?.timeoutMs ?? 2e4
|
|
1540
|
+
});
|
|
1541
|
+
const updates = [];
|
|
1542
|
+
const lines = execRes.stdout.split("\n");
|
|
1543
|
+
for (const line of lines) {
|
|
1544
|
+
if (!line.includes("/") || line.startsWith("Listing")) continue;
|
|
1545
|
+
const match = line.match(/^([^\/\s]+)\/\S+\s+(\S+)\s+\S+\s+\[upgradable from:\s+(\S+)\]/);
|
|
1546
|
+
if (match) {
|
|
1547
|
+
const [, name, latest, current] = match;
|
|
1548
|
+
updates.push({
|
|
1549
|
+
managerId: this.id,
|
|
1550
|
+
managerName: this.name,
|
|
1551
|
+
name,
|
|
1552
|
+
currentVersion: current,
|
|
1553
|
+
latestVersion: latest,
|
|
1554
|
+
type: "deb"
|
|
1555
|
+
});
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
return {
|
|
1559
|
+
managerId: this.id,
|
|
1560
|
+
managerName: this.name,
|
|
1561
|
+
icon: this.icon,
|
|
1562
|
+
category: this.category,
|
|
1563
|
+
available: true,
|
|
1564
|
+
updates,
|
|
1565
|
+
durationMs: Date.now() - startTime
|
|
1566
|
+
};
|
|
1567
|
+
} catch (err) {
|
|
1568
|
+
return {
|
|
1569
|
+
managerId: this.id,
|
|
1570
|
+
managerName: this.name,
|
|
1571
|
+
icon: this.icon,
|
|
1572
|
+
category: this.category,
|
|
1573
|
+
available: true,
|
|
1574
|
+
updates: [],
|
|
1575
|
+
durationMs: Date.now() - startTime,
|
|
1576
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
async executeUpdate(items, options) {
|
|
1581
|
+
const startTime = Date.now();
|
|
1582
|
+
const steps = [];
|
|
1583
|
+
if (!options.dryRun && process.getuid?.() !== 0) {
|
|
1584
|
+
const sudoCheck = await safeExec("sudo", ["-n", "true"], { timeoutMs: 3e3 });
|
|
1585
|
+
if (!sudoCheck.success) {
|
|
1586
|
+
return {
|
|
1587
|
+
managerId: this.id,
|
|
1588
|
+
managerName: this.name,
|
|
1589
|
+
icon: this.icon,
|
|
1590
|
+
success: false,
|
|
1591
|
+
updatedCount: 0,
|
|
1592
|
+
durationMs: Date.now() - startTime,
|
|
1593
|
+
steps: [
|
|
1594
|
+
{
|
|
1595
|
+
name: "sudo privileges",
|
|
1596
|
+
command: "sudo -n true",
|
|
1597
|
+
status: "failed",
|
|
1598
|
+
error: 'APT requires sudo privileges. Run "sudo -v" in your terminal before hmd.'
|
|
1599
|
+
}
|
|
1600
|
+
],
|
|
1601
|
+
error: 'APT requires sudo privileges. Run "sudo -v" before running hmd.'
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
const updateStep = await this.executeStep(
|
|
1606
|
+
"sudo apt-get update",
|
|
1607
|
+
"sudo",
|
|
1608
|
+
["apt-get", "update", "-y"],
|
|
1609
|
+
options
|
|
1610
|
+
);
|
|
1611
|
+
steps.push(updateStep);
|
|
1612
|
+
const upgradeStep = await this.executeStep(
|
|
1613
|
+
"sudo apt-get upgrade",
|
|
1614
|
+
"sudo",
|
|
1615
|
+
["apt-get", "upgrade", "-y"],
|
|
1616
|
+
options
|
|
1617
|
+
);
|
|
1618
|
+
steps.push(upgradeStep);
|
|
1619
|
+
const cleanupStep = await this.executeStep(
|
|
1620
|
+
"sudo apt-get autoremove",
|
|
1621
|
+
"sudo",
|
|
1622
|
+
["apt-get", "autoremove", "-y"],
|
|
1623
|
+
options
|
|
1624
|
+
);
|
|
1625
|
+
steps.push(cleanupStep);
|
|
1626
|
+
const success = steps.every((s) => s.status === "success");
|
|
1627
|
+
return {
|
|
1628
|
+
managerId: this.id,
|
|
1629
|
+
managerName: this.name,
|
|
1630
|
+
icon: this.icon,
|
|
1631
|
+
success,
|
|
1632
|
+
updatedCount: success ? items.length : 0,
|
|
1633
|
+
durationMs: Date.now() - startTime,
|
|
1634
|
+
steps,
|
|
1635
|
+
error: !success ? steps.find((s) => s.status === "failed")?.error : void 0
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
};
|
|
1639
|
+
var FlatpakManager = class extends BasePackageManager {
|
|
1640
|
+
id = "flatpak";
|
|
1641
|
+
name = "Flatpak";
|
|
1642
|
+
icon = "\u{1F4E6}";
|
|
1643
|
+
category = "system";
|
|
1644
|
+
binary = "flatpak";
|
|
1645
|
+
async checkUpdates(options) {
|
|
1646
|
+
const startTime = Date.now();
|
|
1647
|
+
const isAvail = await this.isAvailable();
|
|
1648
|
+
if (!isAvail) {
|
|
1649
|
+
return {
|
|
1650
|
+
managerId: this.id,
|
|
1651
|
+
managerName: this.name,
|
|
1652
|
+
icon: this.icon,
|
|
1653
|
+
category: this.category,
|
|
1654
|
+
available: false,
|
|
1655
|
+
updates: [],
|
|
1656
|
+
durationMs: 0
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
try {
|
|
1660
|
+
const execRes = await safeExec("flatpak", ["remote-ls", "--updates"], {
|
|
1661
|
+
timeoutMs: options?.timeoutMs ?? 2e4
|
|
1662
|
+
});
|
|
1663
|
+
const updates = [];
|
|
1664
|
+
const lines = execRes.stdout.split("\n");
|
|
1665
|
+
for (const line of lines) {
|
|
1666
|
+
const trimmed = line.trim();
|
|
1667
|
+
if (!trimmed) continue;
|
|
1668
|
+
const parts = trimmed.split(/\s+/);
|
|
1669
|
+
updates.push({
|
|
1670
|
+
managerId: this.id,
|
|
1671
|
+
managerName: this.name,
|
|
1672
|
+
name: parts[0],
|
|
1673
|
+
currentVersion: "installed",
|
|
1674
|
+
latestVersion: parts[1] ?? "latest",
|
|
1675
|
+
type: "flatpak"
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
return {
|
|
1679
|
+
managerId: this.id,
|
|
1680
|
+
managerName: this.name,
|
|
1681
|
+
icon: this.icon,
|
|
1682
|
+
category: this.category,
|
|
1683
|
+
available: true,
|
|
1684
|
+
updates,
|
|
1685
|
+
durationMs: Date.now() - startTime
|
|
1686
|
+
};
|
|
1687
|
+
} catch (err) {
|
|
1688
|
+
return {
|
|
1689
|
+
managerId: this.id,
|
|
1690
|
+
managerName: this.name,
|
|
1691
|
+
icon: this.icon,
|
|
1692
|
+
category: this.category,
|
|
1693
|
+
available: true,
|
|
1694
|
+
updates: [],
|
|
1695
|
+
durationMs: Date.now() - startTime,
|
|
1696
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
async executeUpdate(items, options) {
|
|
1701
|
+
const startTime = Date.now();
|
|
1702
|
+
const steps = [];
|
|
1703
|
+
const updateStep = await this.executeStep(
|
|
1704
|
+
"flatpak update -y",
|
|
1705
|
+
"flatpak",
|
|
1706
|
+
["update", "-y", "--noninteractive"],
|
|
1707
|
+
options
|
|
1708
|
+
);
|
|
1709
|
+
steps.push(updateStep);
|
|
1710
|
+
const success = steps.every((s) => s.status === "success");
|
|
1711
|
+
return {
|
|
1712
|
+
managerId: this.id,
|
|
1713
|
+
managerName: this.name,
|
|
1714
|
+
icon: this.icon,
|
|
1715
|
+
success,
|
|
1716
|
+
updatedCount: success ? items.length : 0,
|
|
1717
|
+
durationMs: Date.now() - startTime,
|
|
1718
|
+
steps,
|
|
1719
|
+
error: !success ? steps.find((s) => s.status === "failed")?.error : void 0
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
};
|
|
1723
|
+
|
|
1724
|
+
// src/managers/registry.ts
|
|
1725
|
+
function createDefaultManagers() {
|
|
1726
|
+
return [
|
|
1727
|
+
new HomebrewManager(),
|
|
1728
|
+
new NpmManager(),
|
|
1729
|
+
new PnpmManager(),
|
|
1730
|
+
new BunManager(),
|
|
1731
|
+
new YarnManager(),
|
|
1732
|
+
new PythonPipManager(),
|
|
1733
|
+
new PipxManager(),
|
|
1734
|
+
new CargoManager(),
|
|
1735
|
+
new RubyGemManager(),
|
|
1736
|
+
new MacPortsManager(),
|
|
1737
|
+
new MacAppStoreManager(),
|
|
1738
|
+
new AptManager(),
|
|
1739
|
+
new FlatpakManager()
|
|
1740
|
+
];
|
|
1741
|
+
}
|
|
1742
|
+
function filterManagers(managers, only, exclude) {
|
|
1743
|
+
let result = [...managers];
|
|
1744
|
+
if (only && only.length > 0) {
|
|
1745
|
+
const onlySet = new Set(only.map((s) => s.toLowerCase().trim()));
|
|
1746
|
+
result = result.filter(
|
|
1747
|
+
(m) => onlySet.has(m.id.toLowerCase()) || onlySet.has(m.name.toLowerCase())
|
|
1748
|
+
);
|
|
1749
|
+
}
|
|
1750
|
+
if (exclude && exclude.length > 0) {
|
|
1751
|
+
const excludeSet = new Set(exclude.map((s) => s.toLowerCase().trim()));
|
|
1752
|
+
result = result.filter(
|
|
1753
|
+
(m) => !excludeSet.has(m.id.toLowerCase()) && !excludeSet.has(m.name.toLowerCase())
|
|
1754
|
+
);
|
|
1755
|
+
}
|
|
1756
|
+
return result;
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
// src/core/engine.ts
|
|
1760
|
+
var UpdaterEngine = class {
|
|
1761
|
+
constructor(managers) {
|
|
1762
|
+
this.managers = managers;
|
|
1763
|
+
}
|
|
1764
|
+
managers;
|
|
1765
|
+
/**
|
|
1766
|
+
* Scans all managers for availability and pending updates.
|
|
1767
|
+
* Runs checks concurrently for high performance, but isolates failures.
|
|
1768
|
+
*/
|
|
1769
|
+
async scan(timeoutMs = 25e3, callbacks) {
|
|
1770
|
+
const availabilityChecks = await Promise.all(
|
|
1771
|
+
this.managers.map(async (manager) => {
|
|
1772
|
+
try {
|
|
1773
|
+
const available = await manager.isAvailable();
|
|
1774
|
+
return { manager, available };
|
|
1775
|
+
} catch {
|
|
1776
|
+
return { manager, available: false };
|
|
1777
|
+
}
|
|
1778
|
+
})
|
|
1779
|
+
);
|
|
1780
|
+
const availableManagers = availabilityChecks.filter((c) => c.available).map((c) => c.manager);
|
|
1781
|
+
const unavailableManagers = availabilityChecks.filter((c) => !c.available).map((c) => c.manager);
|
|
1782
|
+
const checkTask = async (manager) => {
|
|
1783
|
+
callbacks?.onManagerStart?.(manager);
|
|
1784
|
+
try {
|
|
1785
|
+
const res = await manager.checkUpdates({ timeoutMs });
|
|
1786
|
+
callbacks?.onManagerComplete?.(manager, res);
|
|
1787
|
+
return res;
|
|
1788
|
+
} catch (err) {
|
|
1789
|
+
const fallbackRes = {
|
|
1790
|
+
managerId: manager.id,
|
|
1791
|
+
managerName: manager.name,
|
|
1792
|
+
icon: manager.icon,
|
|
1793
|
+
category: manager.category,
|
|
1794
|
+
available: true,
|
|
1795
|
+
updates: [],
|
|
1796
|
+
durationMs: 0,
|
|
1797
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1798
|
+
};
|
|
1799
|
+
callbacks?.onManagerComplete?.(manager, fallbackRes);
|
|
1800
|
+
return fallbackRes;
|
|
1801
|
+
}
|
|
1802
|
+
};
|
|
1803
|
+
const checkResults = new Array(availableManagers.length);
|
|
1804
|
+
let taskIndex = 0;
|
|
1805
|
+
const concurrency = Math.min(4, availableManagers.length);
|
|
1806
|
+
const workers = Array.from({ length: concurrency }, async () => {
|
|
1807
|
+
while (taskIndex < availableManagers.length) {
|
|
1808
|
+
const idx = taskIndex++;
|
|
1809
|
+
checkResults[idx] = await checkTask(availableManagers[idx]);
|
|
1810
|
+
}
|
|
1811
|
+
});
|
|
1812
|
+
await Promise.all(workers);
|
|
1813
|
+
const allUpdates = [];
|
|
1814
|
+
for (const res of checkResults) {
|
|
1815
|
+
allUpdates.push(...res.updates);
|
|
1816
|
+
}
|
|
1817
|
+
return {
|
|
1818
|
+
availableResults: checkResults,
|
|
1819
|
+
allUpdates,
|
|
1820
|
+
unavailableManagers
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
/**
|
|
1824
|
+
* Executes updates for the specified managers sequentially or with controlled steps.
|
|
1825
|
+
*/
|
|
1826
|
+
async execute(selectedManagerIds, itemsByManager, options, onManagerStart, onManagerComplete) {
|
|
1827
|
+
const startTime = Date.now();
|
|
1828
|
+
const results = [];
|
|
1829
|
+
const managersToRun = this.managers.filter(
|
|
1830
|
+
(m) => selectedManagerIds.includes(m.id)
|
|
1831
|
+
);
|
|
1832
|
+
for (const manager of managersToRun) {
|
|
1833
|
+
const items = itemsByManager.get(manager.id) ?? [];
|
|
1834
|
+
onManagerStart?.(manager);
|
|
1835
|
+
try {
|
|
1836
|
+
const result = await manager.executeUpdate(items, options);
|
|
1837
|
+
results.push(result);
|
|
1838
|
+
onManagerComplete?.(manager, result);
|
|
1839
|
+
} catch (err) {
|
|
1840
|
+
const failedResult = {
|
|
1841
|
+
managerId: manager.id,
|
|
1842
|
+
managerName: manager.name,
|
|
1843
|
+
icon: manager.icon,
|
|
1844
|
+
success: false,
|
|
1845
|
+
updatedCount: 0,
|
|
1846
|
+
durationMs: 0,
|
|
1847
|
+
steps: [],
|
|
1848
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1849
|
+
};
|
|
1850
|
+
results.push(failedResult);
|
|
1851
|
+
onManagerComplete?.(manager, failedResult);
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
const successful = results.filter((r) => r.success).length;
|
|
1855
|
+
const failed = results.filter((r) => !r.success).length;
|
|
1856
|
+
const totalUpdated = results.reduce((acc, r) => acc + r.updatedCount, 0);
|
|
1857
|
+
const totalPending = Array.from(itemsByManager.values()).reduce(
|
|
1858
|
+
(acc, items) => acc + items.length,
|
|
1859
|
+
0
|
|
1860
|
+
);
|
|
1861
|
+
return {
|
|
1862
|
+
scannedManagersCount: this.managers.length,
|
|
1863
|
+
availableManagersCount: managersToRun.length,
|
|
1864
|
+
upToDateManagersCount: results.filter((r) => r.success && r.updatedCount === 0).length,
|
|
1865
|
+
pendingUpdatesCount: totalPending,
|
|
1866
|
+
executedCount: results.length,
|
|
1867
|
+
successCount: successful,
|
|
1868
|
+
failedCount: failed,
|
|
1869
|
+
totalDurationMs: Date.now() - startTime,
|
|
1870
|
+
results
|
|
1871
|
+
};
|
|
1872
|
+
}
|
|
1873
|
+
};
|
|
1874
|
+
|
|
1875
|
+
// src/utils/formatting.ts
|
|
1876
|
+
import pc from "picocolors";
|
|
1877
|
+
import Table from "cli-table3";
|
|
1878
|
+
function formatDuration(ms) {
|
|
1879
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
1880
|
+
const seconds = (ms / 1e3).toFixed(1);
|
|
1881
|
+
return `${seconds}s`;
|
|
1882
|
+
}
|
|
1883
|
+
function renderUpdatesTable(items) {
|
|
1884
|
+
if (items.length === 0) {
|
|
1885
|
+
return pc.dim(" No updates pending.");
|
|
1886
|
+
}
|
|
1887
|
+
const table = new Table({
|
|
1888
|
+
head: [
|
|
1889
|
+
pc.dim("Manager"),
|
|
1890
|
+
pc.dim("Package"),
|
|
1891
|
+
pc.dim("Type"),
|
|
1892
|
+
pc.dim("Version Transition")
|
|
1893
|
+
],
|
|
1894
|
+
style: {
|
|
1895
|
+
head: [],
|
|
1896
|
+
border: ["dim"]
|
|
1897
|
+
},
|
|
1898
|
+
chars: {
|
|
1899
|
+
top: "\u2500",
|
|
1900
|
+
"top-mid": "\u2500",
|
|
1901
|
+
"top-left": "\u250C",
|
|
1902
|
+
"top-right": "\u2510",
|
|
1903
|
+
bottom: "\u2500",
|
|
1904
|
+
"bottom-mid": "\u2500",
|
|
1905
|
+
"bottom-left": "\u2514",
|
|
1906
|
+
"bottom-right": "\u2518",
|
|
1907
|
+
left: "\u2502",
|
|
1908
|
+
"left-mid": "\u251C",
|
|
1909
|
+
mid: "\u2500",
|
|
1910
|
+
"mid-mid": "\u253C",
|
|
1911
|
+
right: "\u2502",
|
|
1912
|
+
"right-mid": "\u2524",
|
|
1913
|
+
middle: "\u2502"
|
|
1914
|
+
}
|
|
1915
|
+
});
|
|
1916
|
+
for (const item of items) {
|
|
1917
|
+
const typeLabel = item.type ? pc.dim(`[${item.type}]`) : pc.dim("-");
|
|
1918
|
+
const currentVer = pc.yellow(item.currentVersion || "?");
|
|
1919
|
+
const arrow = pc.dim(" \u2192 ");
|
|
1920
|
+
const latestVer = pc.green(pc.bold(item.latestVersion || "?"));
|
|
1921
|
+
table.push([
|
|
1922
|
+
pc.cyan(item.managerName),
|
|
1923
|
+
pc.bold(pc.white(item.name)),
|
|
1924
|
+
typeLabel,
|
|
1925
|
+
`${currentVer}${arrow}${latestVer}`
|
|
1926
|
+
]);
|
|
1927
|
+
}
|
|
1928
|
+
return table.toString();
|
|
1929
|
+
}
|
|
1930
|
+
function renderExecutionSummary(results) {
|
|
1931
|
+
const total = results.length;
|
|
1932
|
+
const successful = results.filter((r) => r.success).length;
|
|
1933
|
+
const failed = results.filter((r) => !r.success).length;
|
|
1934
|
+
const totalUpdated = results.reduce((acc, r) => acc + r.updatedCount, 0);
|
|
1935
|
+
const lines = [];
|
|
1936
|
+
lines.push("");
|
|
1937
|
+
lines.push(pc.dim("\u2500".repeat(60)));
|
|
1938
|
+
lines.push(` ${pc.bold(pc.cyan("Heimdall"))} ${pc.dim("\u2022 Execution Summary")}`);
|
|
1939
|
+
lines.push(pc.dim("\u2500".repeat(60)));
|
|
1940
|
+
for (const res of results) {
|
|
1941
|
+
const icon = res.icon || "\u{1F4E6}";
|
|
1942
|
+
const status = res.success ? pc.green("\u2714 SUCCESS") : pc.red("\u2716 FAILED");
|
|
1943
|
+
const time = pc.dim(`(${formatDuration(res.durationMs)})`);
|
|
1944
|
+
const count = res.success ? pc.dim(`\u2022 ${res.updatedCount} updated`) : pc.dim(`\u2022 ${res.updatedCount} updated, ${res.steps.filter((s) => s.status === "failed").length} issue(s)`);
|
|
1945
|
+
lines.push(` ${icon} ${pc.bold(res.managerName)} ${status} ${count} ${time}`);
|
|
1946
|
+
if (!res.success && res.error) {
|
|
1947
|
+
lines.push(` ${pc.red("\u21B3 Error:")} ${pc.dim(res.error.trim().split("\n")[0])}`);
|
|
1948
|
+
}
|
|
1949
|
+
if (res.steps.length > 0) {
|
|
1950
|
+
for (const step of res.steps) {
|
|
1951
|
+
if (step.status === "skipped") continue;
|
|
1952
|
+
const stepStatus = step.status === "success" ? pc.green("\u2714") : step.status === "failed" ? pc.red("\u2716") : pc.dim("\u25CB");
|
|
1953
|
+
const stepDuration = step.durationMs ? pc.dim(`[${formatDuration(step.durationMs)}]`) : "";
|
|
1954
|
+
const stepError = step.error && step.status === "failed" ? pc.red(` (${step.error.split("\n")[0]})`) : "";
|
|
1955
|
+
lines.push(` ${stepStatus} ${pc.dim(step.name)} ${stepDuration}${stepError}`);
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
lines.push(pc.dim("\u2500".repeat(60)));
|
|
1960
|
+
const summaryParts = [
|
|
1961
|
+
pc.green(`${successful}/${total} managers succeeded`),
|
|
1962
|
+
failed > 0 ? pc.red(`${failed} failed`) : null,
|
|
1963
|
+
pc.cyan(`${totalUpdated} package(s) updated`)
|
|
1964
|
+
].filter(Boolean);
|
|
1965
|
+
lines.push(` ${summaryParts.join(" \u2022 ")}`);
|
|
1966
|
+
lines.push(pc.dim("\u2500".repeat(60)));
|
|
1967
|
+
return lines.join("\n");
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
// src/utils/version.ts
|
|
1971
|
+
import { fileURLToPath } from "url";
|
|
1972
|
+
import { dirname, resolve } from "path";
|
|
1973
|
+
import { existsSync, readFileSync } from "fs";
|
|
1974
|
+
var FALLBACK_METADATA = {
|
|
1975
|
+
name: "@dahch/heimdall",
|
|
1976
|
+
version: "1.0.0",
|
|
1977
|
+
description: "Heimdall: resilient validator and updater for system and global packages"
|
|
1978
|
+
};
|
|
1979
|
+
var cachedMetadata = null;
|
|
1980
|
+
var customFsAdapter = null;
|
|
1981
|
+
function resolvePackageMetadata(options) {
|
|
1982
|
+
const exists = options?.fsAdapter?.existsSync ?? customFsAdapter?.existsSync ?? existsSync;
|
|
1983
|
+
const read = options?.fsAdapter?.readFileSync ?? customFsAdapter?.readFileSync ?? readFileSync;
|
|
1984
|
+
try {
|
|
1985
|
+
let currentDir;
|
|
1986
|
+
if (options?.basePath) {
|
|
1987
|
+
currentDir = options.basePath;
|
|
1988
|
+
} else {
|
|
1989
|
+
try {
|
|
1990
|
+
if (typeof import.meta !== "undefined" && import.meta.url) {
|
|
1991
|
+
currentDir = dirname(fileURLToPath(import.meta.url));
|
|
1992
|
+
}
|
|
1993
|
+
} catch {
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
const candidates = [];
|
|
1997
|
+
if (currentDir) {
|
|
1998
|
+
candidates.push(
|
|
1999
|
+
resolve(currentDir, "../package.json"),
|
|
2000
|
+
resolve(currentDir, "../../package.json"),
|
|
2001
|
+
resolve(currentDir, "./package.json")
|
|
2002
|
+
);
|
|
2003
|
+
}
|
|
2004
|
+
for (const candidate of candidates) {
|
|
2005
|
+
try {
|
|
2006
|
+
if (exists(candidate)) {
|
|
2007
|
+
const raw = read(candidate, "utf-8");
|
|
2008
|
+
const parsed = JSON.parse(raw);
|
|
2009
|
+
if (parsed && typeof parsed === "object") {
|
|
2010
|
+
const name = typeof parsed.name === "string" && parsed.name.trim().length > 0 ? parsed.name.trim() : FALLBACK_METADATA.name;
|
|
2011
|
+
const version = typeof parsed.version === "string" && parsed.version.trim().length > 0 ? parsed.version.trim() : FALLBACK_METADATA.version;
|
|
2012
|
+
const description = typeof parsed.description === "string" && parsed.description.trim().length > 0 ? parsed.description.trim() : FALLBACK_METADATA.description;
|
|
2013
|
+
return { name, version, description };
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
} catch {
|
|
2017
|
+
continue;
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
} catch {
|
|
2021
|
+
}
|
|
2022
|
+
return { ...FALLBACK_METADATA };
|
|
2023
|
+
}
|
|
2024
|
+
function getPackageMetadata(options) {
|
|
2025
|
+
if (!options?.bypassCache && cachedMetadata && !options?.basePath) {
|
|
2026
|
+
return cachedMetadata;
|
|
2027
|
+
}
|
|
2028
|
+
const meta = resolvePackageMetadata(options);
|
|
2029
|
+
if (!options?.basePath) {
|
|
2030
|
+
cachedMetadata = meta;
|
|
2031
|
+
}
|
|
2032
|
+
return meta;
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
// src/cli.ts
|
|
2036
|
+
async function main() {
|
|
2037
|
+
const { version, description } = getPackageMetadata();
|
|
2038
|
+
const program = new Command();
|
|
2039
|
+
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);
|
|
2040
|
+
const options = program.opts();
|
|
2041
|
+
if (process.stdout.isTTY && !options.verbose) {
|
|
2042
|
+
console.clear();
|
|
2043
|
+
}
|
|
2044
|
+
p.intro(`${pc2.cyan("\u25CF")} ${pc2.bold("hmd")} ${pc2.dim(`v${version}`)} \u2014 ${pc2.dim("Heimdall System Updater")}`);
|
|
2045
|
+
const rawTimeout = parseInt(options.timeout || "25000", 10);
|
|
2046
|
+
const timeoutMs = Number.isFinite(rawTimeout) && rawTimeout > 0 ? rawTimeout : 25e3;
|
|
2047
|
+
const onlyList = options.only ? options.only.split(",") : void 0;
|
|
2048
|
+
const excludeList = options.exclude ? options.exclude.split(",") : void 0;
|
|
2049
|
+
const allManagers = createDefaultManagers();
|
|
2050
|
+
const selectedManagers = filterManagers(allManagers, onlyList, excludeList);
|
|
2051
|
+
if (selectedManagers.length === 0) {
|
|
2052
|
+
p.log.error(pc2.red("No package managers matched your filter."));
|
|
2053
|
+
p.outro(pc2.yellow("Exiting without changes."));
|
|
2054
|
+
process.exit(1);
|
|
2055
|
+
}
|
|
2056
|
+
const engine = new UpdaterEngine(selectedManagers);
|
|
2057
|
+
const scanSpinner = p.spinner();
|
|
2058
|
+
scanSpinner.start("Discovering available package managers and checking for updates...");
|
|
2059
|
+
let activeChecks = 0;
|
|
2060
|
+
const { availableResults, allUpdates, unavailableManagers } = await engine.scan(timeoutMs, {
|
|
2061
|
+
onManagerStart: (mgr) => {
|
|
2062
|
+
activeChecks++;
|
|
2063
|
+
scanSpinner.message(`Querying ${mgr.icon} ${mgr.name} (${activeChecks} in progress)...`);
|
|
2064
|
+
},
|
|
2065
|
+
onManagerComplete: (mgr, res) => {
|
|
2066
|
+
activeChecks = Math.max(0, activeChecks - 1);
|
|
2067
|
+
const countStr = res.updates.length > 0 ? pc2.yellow(`${res.updates.length} outdated`) : pc2.green("up to date");
|
|
2068
|
+
scanSpinner.message(`Checked ${mgr.icon} ${mgr.name}: ${countStr}`);
|
|
2069
|
+
}
|
|
2070
|
+
});
|
|
2071
|
+
scanSpinner.stop("Discovery and validation completed");
|
|
2072
|
+
const availableManagers = availableResults.map((r) => `${r.icon} ${pc2.bold(r.managerName)}`);
|
|
2073
|
+
p.log.info(
|
|
2074
|
+
`${pc2.bold("Active package managers:")} ${availableManagers.join(" \u2022 ")}`
|
|
2075
|
+
);
|
|
2076
|
+
if (unavailableManagers.length > 0 && options.verbose) {
|
|
2077
|
+
const unavailStr = unavailableManagers.map((m) => `${m.icon} ${m.name}`).join(", ");
|
|
2078
|
+
p.log.step(pc2.dim(`Inactive/not installed (skipped safely): ${unavailStr}`));
|
|
2079
|
+
}
|
|
2080
|
+
if (allUpdates.length === 0) {
|
|
2081
|
+
p.note(
|
|
2082
|
+
pc2.green("All detected package managers and global packages are completely up to date! \u{1F389}"),
|
|
2083
|
+
"Status: Up to Date"
|
|
2084
|
+
);
|
|
2085
|
+
p.outro(pc2.cyan("No updates needed. Have a great day!"));
|
|
2086
|
+
process.exit(0);
|
|
2087
|
+
}
|
|
2088
|
+
p.log.message(`
|
|
2089
|
+
${pc2.bold("Pending updates")} ${pc2.dim(`(${allUpdates.length} total):`)}`);
|
|
2090
|
+
console.log(renderUpdatesTable(allUpdates));
|
|
2091
|
+
console.log("");
|
|
2092
|
+
const itemsByManager = /* @__PURE__ */ new Map();
|
|
2093
|
+
for (const update of allUpdates) {
|
|
2094
|
+
const list = itemsByManager.get(update.managerId) ?? [];
|
|
2095
|
+
list.push(update);
|
|
2096
|
+
itemsByManager.set(update.managerId, list);
|
|
2097
|
+
}
|
|
2098
|
+
let managersToUpdate = [];
|
|
2099
|
+
if (options.dryRun) {
|
|
2100
|
+
managersToUpdate = Array.from(itemsByManager.keys());
|
|
2101
|
+
p.log.info(pc2.yellow(`[DRY RUN] Simulating updates for ${allUpdates.length} package(s)...`));
|
|
2102
|
+
} else if (options.yes) {
|
|
2103
|
+
managersToUpdate = Array.from(itemsByManager.keys());
|
|
2104
|
+
p.log.info(pc2.cyan("Flag -y provided: proceeding with all updates automatically."));
|
|
2105
|
+
} else {
|
|
2106
|
+
const managerOptions = Array.from(itemsByManager.entries()).map(([mgrId, items]) => {
|
|
2107
|
+
const mgrInfo = availableResults.find((r) => r.managerId === mgrId);
|
|
2108
|
+
return {
|
|
2109
|
+
value: mgrId,
|
|
2110
|
+
label: `${mgrInfo?.icon ?? "\u{1F4E6}"} ${mgrInfo?.managerName ?? mgrId} (${items.length} updates)`
|
|
2111
|
+
};
|
|
2112
|
+
});
|
|
2113
|
+
const action = await p.select({
|
|
2114
|
+
message: `How would you like to proceed with the ${allUpdates.length} update(s)?`,
|
|
2115
|
+
options: [
|
|
2116
|
+
{
|
|
2117
|
+
value: "all",
|
|
2118
|
+
label: `\u26A1 Confirm & Update everything (${allUpdates.length} packages across ${itemsByManager.size} managers)`,
|
|
2119
|
+
hint: "Recommended"
|
|
2120
|
+
},
|
|
2121
|
+
{
|
|
2122
|
+
value: "select",
|
|
2123
|
+
label: "\u2699 Select specific package managers to update",
|
|
2124
|
+
hint: "Fine-grained choice"
|
|
2125
|
+
},
|
|
2126
|
+
{
|
|
2127
|
+
value: "cancel",
|
|
2128
|
+
label: "\u2716 Cancel and exit without updating"
|
|
2129
|
+
}
|
|
2130
|
+
]
|
|
2131
|
+
});
|
|
2132
|
+
if (p.isCancel(action) || action === "cancel") {
|
|
2133
|
+
p.cancel("Update operation cancelled by user.");
|
|
2134
|
+
process.exit(0);
|
|
2135
|
+
}
|
|
2136
|
+
if (action === "all") {
|
|
2137
|
+
managersToUpdate = Array.from(itemsByManager.keys());
|
|
2138
|
+
} else if (action === "select") {
|
|
2139
|
+
const chosen = await p.multiselect({
|
|
2140
|
+
message: "Select the package managers you want to update:",
|
|
2141
|
+
options: managerOptions,
|
|
2142
|
+
initialValues: Array.from(itemsByManager.keys()),
|
|
2143
|
+
required: true
|
|
2144
|
+
});
|
|
2145
|
+
if (p.isCancel(chosen)) {
|
|
2146
|
+
p.cancel("Selection cancelled.");
|
|
2147
|
+
process.exit(0);
|
|
2148
|
+
}
|
|
2149
|
+
managersToUpdate = chosen;
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
if (managersToUpdate.length === 0) {
|
|
2153
|
+
p.outro(pc2.yellow("No managers selected for update."));
|
|
2154
|
+
process.exit(0);
|
|
2155
|
+
}
|
|
2156
|
+
const isSimulated = options.dryRun ?? false;
|
|
2157
|
+
p.log.step(
|
|
2158
|
+
pc2.bold(
|
|
2159
|
+
pc2.cyan(
|
|
2160
|
+
`
|
|
2161
|
+
${isSimulated ? "[DRY RUN] Simulating" : "Starting"} updates for ${managersToUpdate.length} package manager(s)...`
|
|
2162
|
+
)
|
|
2163
|
+
)
|
|
2164
|
+
);
|
|
2165
|
+
const updateSpinner = p.spinner();
|
|
2166
|
+
let managerIndex = 0;
|
|
2167
|
+
const summary = await engine.execute(
|
|
2168
|
+
managersToUpdate,
|
|
2169
|
+
itemsByManager,
|
|
2170
|
+
{
|
|
2171
|
+
verbose: options.verbose,
|
|
2172
|
+
dryRun: isSimulated,
|
|
2173
|
+
onStepStart: (stepName) => {
|
|
2174
|
+
updateSpinner.message(pc2.dim(`Running: ${stepName}...`));
|
|
2175
|
+
},
|
|
2176
|
+
onStepProgress: (stepName, logLine) => {
|
|
2177
|
+
if (logLine) {
|
|
2178
|
+
const truncated = logLine.length > 55 ? logLine.slice(0, 52) + "..." : logLine;
|
|
2179
|
+
updateSpinner.message(pc2.dim(`${stepName} [${truncated}]`));
|
|
2180
|
+
}
|
|
2181
|
+
},
|
|
2182
|
+
onStepEnd: (stepName, success, error, durationMs) => {
|
|
2183
|
+
const mark = success ? pc2.green("\u2714") : pc2.red("\u2716");
|
|
2184
|
+
const durStr = durationMs !== void 0 ? pc2.dim(` [${formatDuration(durationMs)}]`) : "";
|
|
2185
|
+
const detail = error ? pc2.red(` \u21B3 ${error.split("\n")[0]}`) : "";
|
|
2186
|
+
updateSpinner.stop(` ${mark} ${pc2.dim(stepName)}${durStr}${detail}`);
|
|
2187
|
+
updateSpinner.start(pc2.dim("Working..."));
|
|
2188
|
+
}
|
|
2189
|
+
},
|
|
2190
|
+
(mgr) => {
|
|
2191
|
+
managerIndex++;
|
|
2192
|
+
console.log(pc2.bold(pc2.cyan(`
|
|
2193
|
+
\u25CF [${managerIndex}/${managersToUpdate.length}] ${mgr.icon} ${mgr.name}`)));
|
|
2194
|
+
updateSpinner.start(pc2.dim("Starting update routine..."));
|
|
2195
|
+
},
|
|
2196
|
+
(mgr, res) => {
|
|
2197
|
+
if (res.success) {
|
|
2198
|
+
updateSpinner.stop(pc2.green(` \u2714 ${mgr.name} completed in ${formatDuration(res.durationMs)}`));
|
|
2199
|
+
} else {
|
|
2200
|
+
updateSpinner.stop(pc2.red(` \u2716 ${mgr.name} completed with issues in ${formatDuration(res.durationMs)}`));
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
);
|
|
2204
|
+
console.log(renderExecutionSummary(summary.results));
|
|
2205
|
+
if (summary.failedCount === 0) {
|
|
2206
|
+
p.outro(
|
|
2207
|
+
pc2.bold(
|
|
2208
|
+
pc2.green(
|
|
2209
|
+
`\u2714 All operations completed successfully: ${summary.pendingUpdatesCount} package(s) updated across ${summary.successCount} manager(s) (${formatDuration(summary.totalDurationMs)})!`
|
|
2210
|
+
)
|
|
2211
|
+
)
|
|
2212
|
+
);
|
|
2213
|
+
} else {
|
|
2214
|
+
process.exitCode = 1;
|
|
2215
|
+
p.outro(
|
|
2216
|
+
pc2.bold(
|
|
2217
|
+
pc2.yellow(
|
|
2218
|
+
`Completed with warnings: ${summary.successCount} succeeded, ${summary.failedCount} failed (${formatDuration(summary.totalDurationMs)}).`
|
|
2219
|
+
)
|
|
2220
|
+
)
|
|
2221
|
+
);
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
main().catch((err) => {
|
|
2225
|
+
p.log.error(pc2.red(`Fatal error: ${err instanceof Error ? err.message : String(err)}`));
|
|
2226
|
+
process.exit(1);
|
|
2227
|
+
});
|