brew-tui 0.4.1 → 0.5.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 +50 -16
- package/build/{brewbar-installer-6BR3MPVZ.js → brewbar-installer-ZEMXNDHP.js} +4 -3
- package/build/{brewbar-installer-6BR3MPVZ.js.map → brewbar-installer-ZEMXNDHP.js.map} +1 -1
- package/build/brewfile-manager-3SERRYNC.js +20 -0
- package/build/chunk-42URLVAJ.js +299 -0
- package/build/chunk-42URLVAJ.js.map +1 -0
- package/build/chunk-4I344KQX.js +221 -0
- package/build/chunk-4I344KQX.js.map +1 -0
- package/build/chunk-KDHEUNRI.js +62 -0
- package/build/chunk-KDHEUNRI.js.map +1 -0
- package/build/{chunk-E7ZREAJW.js → chunk-KDJNCZD7.js} +237 -17
- package/build/chunk-KDJNCZD7.js.map +1 -0
- package/build/chunk-KN4GCMIE.js +48 -0
- package/build/chunk-KN4GCMIE.js.map +1 -0
- package/build/{chunk-65YZJX2E.js → chunk-KVCVIRWI.js} +6 -20
- package/build/chunk-KVCVIRWI.js.map +1 -0
- package/build/chunk-LXF72RCD.js +467 -0
- package/build/chunk-LXF72RCD.js.map +1 -0
- package/build/chunk-U2DRWB7A.js +123 -0
- package/build/chunk-U2DRWB7A.js.map +1 -0
- package/build/chunk-UWS4A4F5.js +25 -0
- package/build/chunk-UWS4A4F5.js.map +1 -0
- package/build/compliance-checker-X7P623UF.js +12 -0
- package/build/compliance-checker-X7P623UF.js.map +1 -0
- package/build/{history-logger-2PGYSPFL.js → history-logger-PBDOLKNJ.js} +3 -2
- package/build/history-logger-PBDOLKNJ.js.map +1 -0
- package/build/index.js +2003 -453
- package/build/index.js.map +1 -1
- package/build/policy-io-EECGRKNA.js +11 -0
- package/build/policy-io-EECGRKNA.js.map +1 -0
- package/build/snapshot-RAPGMAJF.js +17 -0
- package/build/snapshot-RAPGMAJF.js.map +1 -0
- package/build/sync-engine-4ERSW4EQ.js +18 -0
- package/build/sync-engine-4ERSW4EQ.js.map +1 -0
- package/package.json +11 -9
- package/build/chunk-65YZJX2E.js.map +0 -1
- package/build/chunk-E7ZREAJW.js.map +0 -1
- /package/build/{history-logger-2PGYSPFL.js.map → brewfile-manager-3SERRYNC.js.map} +0 -0
package/build/index.js
CHANGED
|
@@ -1,21 +1,54 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
computeDrift,
|
|
3
|
+
createDefaultBrewfile,
|
|
4
|
+
diffSnapshots,
|
|
5
|
+
loadBrewfile,
|
|
6
|
+
reconcile,
|
|
7
|
+
saveBrewfile
|
|
8
|
+
} from "./chunk-LXF72RCD.js";
|
|
9
|
+
import {
|
|
10
|
+
applyConflictResolutions,
|
|
11
|
+
decryptPayload,
|
|
12
|
+
loadSyncConfig,
|
|
13
|
+
readSyncEnvelope,
|
|
14
|
+
sync
|
|
15
|
+
} from "./chunk-42URLVAJ.js";
|
|
16
|
+
import {
|
|
17
|
+
checkCompliance
|
|
18
|
+
} from "./chunk-U2DRWB7A.js";
|
|
19
|
+
import {
|
|
20
|
+
captureSnapshot,
|
|
21
|
+
execBrew,
|
|
22
|
+
loadSnapshots,
|
|
23
|
+
saveSnapshot,
|
|
24
|
+
streamBrew
|
|
25
|
+
} from "./chunk-4I344KQX.js";
|
|
26
|
+
import {
|
|
27
|
+
exportReport,
|
|
28
|
+
loadPolicy
|
|
29
|
+
} from "./chunk-KN4GCMIE.js";
|
|
30
|
+
import {
|
|
5
31
|
appendEntry,
|
|
6
32
|
clearHistory,
|
|
7
33
|
detectAction,
|
|
8
|
-
ensureDataDirs,
|
|
9
34
|
loadHistory
|
|
10
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-KVCVIRWI.js";
|
|
36
|
+
import {
|
|
37
|
+
DATA_DIR,
|
|
38
|
+
LICENSE_PATH,
|
|
39
|
+
PROFILES_DIR,
|
|
40
|
+
ensureDataDirs
|
|
41
|
+
} from "./chunk-UWS4A4F5.js";
|
|
11
42
|
import {
|
|
12
43
|
fetchWithTimeout,
|
|
13
44
|
getLocale,
|
|
14
|
-
logger,
|
|
15
45
|
t,
|
|
16
46
|
tp,
|
|
17
47
|
useLocaleStore
|
|
18
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-KDJNCZD7.js";
|
|
49
|
+
import {
|
|
50
|
+
logger
|
|
51
|
+
} from "./chunk-KDHEUNRI.js";
|
|
19
52
|
|
|
20
53
|
// src/index.tsx
|
|
21
54
|
import { createInterface } from "readline/promises";
|
|
@@ -23,7 +56,7 @@ import { rm as rm3 } from "fs/promises";
|
|
|
23
56
|
import { render } from "ink";
|
|
24
57
|
|
|
25
58
|
// src/app.tsx
|
|
26
|
-
import { useEffect as
|
|
59
|
+
import { useEffect as useEffect19 } from "react";
|
|
27
60
|
import { useApp } from "ink";
|
|
28
61
|
|
|
29
62
|
// src/components/layout/app-layout.tsx
|
|
@@ -44,7 +77,11 @@ var VIEWS = [
|
|
|
44
77
|
"profiles",
|
|
45
78
|
"smart-cleanup",
|
|
46
79
|
"history",
|
|
80
|
+
"rollback",
|
|
81
|
+
"brewfile",
|
|
82
|
+
"sync",
|
|
47
83
|
"security-audit",
|
|
84
|
+
"compliance",
|
|
48
85
|
"account"
|
|
49
86
|
];
|
|
50
87
|
var useNavigationStore = create((set, get) => ({
|
|
@@ -85,11 +122,18 @@ var PRO_VIEWS = /* @__PURE__ */ new Set([
|
|
|
85
122
|
"profiles",
|
|
86
123
|
"smart-cleanup",
|
|
87
124
|
"history",
|
|
125
|
+
"rollback",
|
|
126
|
+
"brewfile",
|
|
127
|
+
"sync",
|
|
88
128
|
"security-audit"
|
|
89
129
|
]);
|
|
130
|
+
var TEAM_VIEWS = /* @__PURE__ */ new Set(["compliance"]);
|
|
90
131
|
function isProView(viewId) {
|
|
91
132
|
return PRO_VIEWS.has(viewId);
|
|
92
133
|
}
|
|
134
|
+
function isTeamView(viewId) {
|
|
135
|
+
return TEAM_VIEWS.has(viewId);
|
|
136
|
+
}
|
|
93
137
|
|
|
94
138
|
// src/utils/colors.ts
|
|
95
139
|
var COLORS = {
|
|
@@ -187,7 +231,11 @@ var VIEW_LABEL_KEYS = {
|
|
|
187
231
|
profiles: "view_profiles",
|
|
188
232
|
"smart-cleanup": "view_smartCleanup",
|
|
189
233
|
history: "view_history",
|
|
234
|
+
rollback: "view_rollback",
|
|
235
|
+
brewfile: "view_brewfile",
|
|
236
|
+
sync: "view_sync",
|
|
190
237
|
"security-audit": "view_securityAudit",
|
|
238
|
+
compliance: "view_compliance",
|
|
191
239
|
account: "view_account"
|
|
192
240
|
};
|
|
193
241
|
var VIEW_KEYS = {
|
|
@@ -202,6 +250,10 @@ var VIEW_KEYS = {
|
|
|
202
250
|
"smart-cleanup": "7",
|
|
203
251
|
history: "8",
|
|
204
252
|
"security-audit": "9",
|
|
253
|
+
rollback: "",
|
|
254
|
+
brewfile: "",
|
|
255
|
+
sync: "",
|
|
256
|
+
compliance: "",
|
|
205
257
|
account: "0"
|
|
206
258
|
};
|
|
207
259
|
var TAB_VIEWS = [
|
|
@@ -214,13 +266,17 @@ var TAB_VIEWS = [
|
|
|
214
266
|
"profiles",
|
|
215
267
|
"smart-cleanup",
|
|
216
268
|
"history",
|
|
269
|
+
"rollback",
|
|
270
|
+
"brewfile",
|
|
271
|
+
"sync",
|
|
217
272
|
"security-audit",
|
|
273
|
+
"compliance",
|
|
218
274
|
"account"
|
|
219
275
|
];
|
|
220
276
|
function MenuItem({ view, currentView }) {
|
|
221
277
|
const key = VIEW_KEYS[view];
|
|
222
278
|
const viewLabel = t(VIEW_LABEL_KEYS[view]);
|
|
223
|
-
const isPro = isProView(view);
|
|
279
|
+
const isPro = isProView(view) || isTeamView(view);
|
|
224
280
|
const isActive = view === currentView;
|
|
225
281
|
const isAccount = view === "account";
|
|
226
282
|
const indicator = key || (view === "package-info" ? "\u21B2" : " ");
|
|
@@ -303,6 +359,10 @@ var VIEW_HINT_DEFS = {
|
|
|
303
359
|
"smart-cleanup": [["enter", "hint_toggle"], ["c", "hint_clean"], ["r", "hint_refresh"], ["S", "hint_search"], ["q", "hint_quit"]],
|
|
304
360
|
history: [["/", "hint_search"], ["enter", "hint_replay"], ["f", "hint_filter"], ["c", "hint_clear"], ["q", "hint_quit"]],
|
|
305
361
|
"security-audit": [["r", "hint_scan"], ["enter", "hint_details"], ["u", "hint_upgrade"], ["S", "hint_search"], ["q", "hint_quit"]],
|
|
362
|
+
rollback: [["j/k", "hint_navigate"], ["enter", "hint_select"], ["r", "hint_rollback_confirm"], ["esc", "hint_back"], ["q", "hint_quit"]],
|
|
363
|
+
brewfile: [["j/k", "hint_navigate"], ["a", "hint_add"], ["d", "hint_delete"], ["r", "hint_reconcile"], ["e", "hint_export"], ["q", "hint_quit"]],
|
|
364
|
+
sync: [["s", "hint_sync"], ["r", "hint_refresh"], ["c", "hint_conflict"], ["esc", "hint_back"], ["q", "hint_quit"]],
|
|
365
|
+
compliance: [["r", "hint_scan"], ["i", "hint_import"], ["e", "hint_export"], ["c", "hint_clean"], ["q", "hint_quit"]],
|
|
306
366
|
account: [["p", "hint_promo"], ["d", "hint_deactivate"], ["S", "hint_search"], ["q", "hint_quit"]]
|
|
307
367
|
};
|
|
308
368
|
function HintItem({ def }) {
|
|
@@ -485,6 +545,8 @@ async function deactivateLicense(key, instanceId) {
|
|
|
485
545
|
// src/lib/license/license-manager.ts
|
|
486
546
|
var BUILTIN_ACCOUNTS = {
|
|
487
547
|
"admin@molinesdesigns.com": "pro",
|
|
548
|
+
"team@molinesdesigns.com": "team",
|
|
549
|
+
// Team tier test/admin account
|
|
488
550
|
"artax1983@icloud.com": "free"
|
|
489
551
|
};
|
|
490
552
|
function getBuiltinAccountType(email) {
|
|
@@ -568,9 +630,9 @@ function isEncryptedLicenseFile(obj) {
|
|
|
568
630
|
async function getMachineId2() {
|
|
569
631
|
try {
|
|
570
632
|
const { readFile: readMachineId } = await import("fs/promises");
|
|
571
|
-
const { join:
|
|
633
|
+
const { join: join6 } = await import("path");
|
|
572
634
|
const { homedir: homedir3 } = await import("os");
|
|
573
|
-
const machineIdPath =
|
|
635
|
+
const machineIdPath = join6(homedir3(), ".brew-tui", "machine-id");
|
|
574
636
|
return (await readMachineId(machineIdPath, "utf-8")).trim() || null;
|
|
575
637
|
} catch {
|
|
576
638
|
return null;
|
|
@@ -768,6 +830,10 @@ var useLicenseStore = create2((set, get) => ({
|
|
|
768
830
|
set({ status: "pro", license: { ...license, status: "active" }, degradation: "none" });
|
|
769
831
|
return;
|
|
770
832
|
}
|
|
833
|
+
if (builtinType === "team") {
|
|
834
|
+
set({ status: "team", license: { ...license, status: "active", plan: "team" }, degradation: "none" });
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
771
837
|
if (builtinType === "free") {
|
|
772
838
|
set({ status: "free", license: null, degradation: "none" });
|
|
773
839
|
return;
|
|
@@ -825,7 +891,15 @@ var useLicenseStore = create2((set, get) => ({
|
|
|
825
891
|
}
|
|
826
892
|
set({ status: "free", license: null, degradation: "none", error: null });
|
|
827
893
|
},
|
|
828
|
-
|
|
894
|
+
// Team is a superset of Pro — team users have full Pro access plus team features
|
|
895
|
+
isPro: () => {
|
|
896
|
+
const s = get().status;
|
|
897
|
+
return s === "pro" || s === "team";
|
|
898
|
+
},
|
|
899
|
+
isTeam: () => {
|
|
900
|
+
const s = get().status;
|
|
901
|
+
return s === "team" || s === "pro";
|
|
902
|
+
}
|
|
829
903
|
}));
|
|
830
904
|
|
|
831
905
|
// src/hooks/use-keyboard.ts
|
|
@@ -905,7 +979,9 @@ var FEATURE_KEYS = {
|
|
|
905
979
|
profiles: { title: "upgrade_profiles", desc: "upgrade_profilesDesc" },
|
|
906
980
|
"smart-cleanup": { title: "upgrade_cleanup", desc: "upgrade_cleanupDesc" },
|
|
907
981
|
history: { title: "upgrade_history", desc: "upgrade_historyDesc" },
|
|
908
|
-
"security-audit": { title: "upgrade_security", desc: "upgrade_securityDesc" }
|
|
982
|
+
"security-audit": { title: "upgrade_security", desc: "upgrade_securityDesc" },
|
|
983
|
+
sync: { title: "upgrade_sync", desc: "upgrade_syncDesc" },
|
|
984
|
+
compliance: { title: "upgrade_compliance", desc: "upgrade_complianceDesc" }
|
|
909
985
|
};
|
|
910
986
|
function UpgradePrompt({ viewId }) {
|
|
911
987
|
const keys = FEATURE_KEYS[viewId];
|
|
@@ -960,98 +1036,7 @@ import { Box as Box8, Text as Text10, useStdout as useStdout3 } from "ink";
|
|
|
960
1036
|
import { create as create4 } from "zustand";
|
|
961
1037
|
|
|
962
1038
|
// src/lib/brew-api.ts
|
|
963
|
-
import { spawn as spawn2 } from "child_process";
|
|
964
|
-
|
|
965
|
-
// src/lib/brew-cli.ts
|
|
966
1039
|
import { spawn } from "child_process";
|
|
967
|
-
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
968
|
-
var STREAM_IDLE_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
969
|
-
async function execBrew(args, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
970
|
-
return new Promise((resolve, reject) => {
|
|
971
|
-
const proc = spawn("brew", args, { env: { ...process.env, HOMEBREW_NO_AUTO_UPDATE: "1" } });
|
|
972
|
-
let stdout = "";
|
|
973
|
-
let stderr = "";
|
|
974
|
-
let killed = false;
|
|
975
|
-
const timer = setTimeout(() => {
|
|
976
|
-
killed = true;
|
|
977
|
-
proc.kill();
|
|
978
|
-
reject(new Error(`brew ${args.join(" ")} timed out after ${timeoutMs}ms`));
|
|
979
|
-
}, timeoutMs);
|
|
980
|
-
proc.stdout.on("data", (d) => {
|
|
981
|
-
stdout += d.toString();
|
|
982
|
-
});
|
|
983
|
-
proc.stderr.on("data", (d) => {
|
|
984
|
-
stderr += d.toString();
|
|
985
|
-
});
|
|
986
|
-
proc.on("close", (code) => {
|
|
987
|
-
clearTimeout(timer);
|
|
988
|
-
if (killed) return;
|
|
989
|
-
if (code === 0) {
|
|
990
|
-
resolve(stdout);
|
|
991
|
-
} else {
|
|
992
|
-
reject(new Error(stderr.trim() || `brew ${args.join(" ")} exited with code ${code}`));
|
|
993
|
-
}
|
|
994
|
-
});
|
|
995
|
-
proc.on("error", (err) => {
|
|
996
|
-
clearTimeout(timer);
|
|
997
|
-
if (killed) return;
|
|
998
|
-
reject(new Error(`Failed to run brew: ${err.message}`));
|
|
999
|
-
});
|
|
1000
|
-
});
|
|
1001
|
-
}
|
|
1002
|
-
async function* streamBrew(args) {
|
|
1003
|
-
const proc = spawn("brew", args, {
|
|
1004
|
-
env: { ...process.env, HOMEBREW_NO_AUTO_UPDATE: "1" },
|
|
1005
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
1006
|
-
});
|
|
1007
|
-
let buffer = "";
|
|
1008
|
-
const lines = [];
|
|
1009
|
-
let done = false;
|
|
1010
|
-
let exitError = null;
|
|
1011
|
-
let lastOutputAt = Date.now();
|
|
1012
|
-
const push = (chunk) => {
|
|
1013
|
-
lastOutputAt = Date.now();
|
|
1014
|
-
buffer += chunk.toString();
|
|
1015
|
-
const parts = buffer.split("\n");
|
|
1016
|
-
buffer = parts.pop() ?? "";
|
|
1017
|
-
for (const line of parts) {
|
|
1018
|
-
if (line.trim()) lines.push(line);
|
|
1019
|
-
}
|
|
1020
|
-
};
|
|
1021
|
-
proc.stdout.on("data", push);
|
|
1022
|
-
proc.stderr.on("data", push);
|
|
1023
|
-
proc.on("close", (code) => {
|
|
1024
|
-
if (buffer.trim()) lines.push(buffer.trim());
|
|
1025
|
-
done = true;
|
|
1026
|
-
if (code !== 0) {
|
|
1027
|
-
exitError = `brew ${args.join(" ")} exited with code ${code}`;
|
|
1028
|
-
}
|
|
1029
|
-
});
|
|
1030
|
-
proc.on("error", (err) => {
|
|
1031
|
-
done = true;
|
|
1032
|
-
exitError = err.message;
|
|
1033
|
-
});
|
|
1034
|
-
try {
|
|
1035
|
-
while (!done || lines.length > 0) {
|
|
1036
|
-
if (lines.length > 0) {
|
|
1037
|
-
yield lines.shift();
|
|
1038
|
-
} else if (!done) {
|
|
1039
|
-
if (Date.now() - lastOutputAt > STREAM_IDLE_TIMEOUT_MS) {
|
|
1040
|
-
proc.kill();
|
|
1041
|
-
throw new Error(`brew ${args.join(" ")} timed out: no output for ${STREAM_IDLE_TIMEOUT_MS / 1e3}s`);
|
|
1042
|
-
}
|
|
1043
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
} finally {
|
|
1047
|
-
if (!done) {
|
|
1048
|
-
proc.kill();
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
if (exitError) {
|
|
1052
|
-
throw new Error(exitError);
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
1040
|
|
|
1056
1041
|
// src/lib/parsers/json-parser.ts
|
|
1057
1042
|
function safeParse(raw, context) {
|
|
@@ -1062,7 +1047,7 @@ function safeParse(raw, context) {
|
|
|
1062
1047
|
}
|
|
1063
1048
|
return result;
|
|
1064
1049
|
} catch (err) {
|
|
1065
|
-
throw new Error(`Failed to parse ${context} JSON: ${err instanceof Error ? err.message : String(err)}
|
|
1050
|
+
throw new Error(`Failed to parse ${context} JSON: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
1066
1051
|
}
|
|
1067
1052
|
}
|
|
1068
1053
|
function parseInstalledJson(raw) {
|
|
@@ -1150,6 +1135,97 @@ function parseLeavesOutput(raw) {
|
|
|
1150
1135
|
return raw.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
1151
1136
|
}
|
|
1152
1137
|
|
|
1138
|
+
// src/lib/impact/impact-analyzer.ts
|
|
1139
|
+
var HIGH_RISK_PACKAGES = /* @__PURE__ */ new Set([
|
|
1140
|
+
"openssl",
|
|
1141
|
+
"openssl@3",
|
|
1142
|
+
"openssl@1.1",
|
|
1143
|
+
"python",
|
|
1144
|
+
"python@3",
|
|
1145
|
+
"python@3.11",
|
|
1146
|
+
"python@3.12",
|
|
1147
|
+
"python@3.13",
|
|
1148
|
+
"node",
|
|
1149
|
+
"node@18",
|
|
1150
|
+
"node@20",
|
|
1151
|
+
"ruby",
|
|
1152
|
+
"ruby@3",
|
|
1153
|
+
"sqlite",
|
|
1154
|
+
"sqlite3",
|
|
1155
|
+
"libpq",
|
|
1156
|
+
"postgresql",
|
|
1157
|
+
"postgresql@16",
|
|
1158
|
+
"glibc",
|
|
1159
|
+
"gcc",
|
|
1160
|
+
"llvm"
|
|
1161
|
+
]);
|
|
1162
|
+
function isMajorVersionBump(from, to) {
|
|
1163
|
+
const fromMajor = parseInt(from.split(".")[0] ?? "0", 10);
|
|
1164
|
+
const toMajor = parseInt(to.split(".")[0] ?? "0", 10);
|
|
1165
|
+
return !isNaN(fromMajor) && !isNaN(toMajor) && toMajor > fromMajor;
|
|
1166
|
+
}
|
|
1167
|
+
function calculateRisk(name, reverseDeps, fromVersion, toVersion) {
|
|
1168
|
+
const reasons = [];
|
|
1169
|
+
if (HIGH_RISK_PACKAGES.has(name)) {
|
|
1170
|
+
reasons.push(t("impact_reason_critical_package"));
|
|
1171
|
+
return { risk: "high", reasons };
|
|
1172
|
+
}
|
|
1173
|
+
if (reverseDeps.length > 10) {
|
|
1174
|
+
reasons.push(t("impact_reason_many_deps", { count: reverseDeps.length }));
|
|
1175
|
+
return { risk: "high", reasons };
|
|
1176
|
+
}
|
|
1177
|
+
let factorCount = 0;
|
|
1178
|
+
if (reverseDeps.length >= 3) {
|
|
1179
|
+
factorCount++;
|
|
1180
|
+
reasons.push(t("impact_reason_many_deps", { count: reverseDeps.length }));
|
|
1181
|
+
}
|
|
1182
|
+
if (isMajorVersionBump(fromVersion, toVersion)) {
|
|
1183
|
+
factorCount++;
|
|
1184
|
+
reasons.push(t("impact_reason_major_bump"));
|
|
1185
|
+
}
|
|
1186
|
+
const risk = factorCount >= 2 ? "high" : factorCount === 1 ? "medium" : "low";
|
|
1187
|
+
return { risk, reasons };
|
|
1188
|
+
}
|
|
1189
|
+
async function analyzeUpgradeImpact(packageName, fromVersion, toVersion, packageType) {
|
|
1190
|
+
if (packageType === "cask") {
|
|
1191
|
+
return {
|
|
1192
|
+
packageName,
|
|
1193
|
+
fromVersion,
|
|
1194
|
+
toVersion,
|
|
1195
|
+
packageType,
|
|
1196
|
+
directDeps: [],
|
|
1197
|
+
reverseDeps: [],
|
|
1198
|
+
risk: "low",
|
|
1199
|
+
riskReasons: []
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
let directDeps = [];
|
|
1203
|
+
let reverseDeps = [];
|
|
1204
|
+
try {
|
|
1205
|
+
const depsOutput = await execBrew(["deps", "--1", packageName]);
|
|
1206
|
+
directDeps = depsOutput.split("\n").filter((l) => l.trim() !== "");
|
|
1207
|
+
} catch (err) {
|
|
1208
|
+
logger.warn(`impact-analyzer: deps failed for ${packageName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1209
|
+
}
|
|
1210
|
+
try {
|
|
1211
|
+
const usesOutput = await execBrew(["uses", "--installed", packageName]);
|
|
1212
|
+
reverseDeps = usesOutput.split("\n").filter((l) => l.trim() !== "");
|
|
1213
|
+
} catch (err) {
|
|
1214
|
+
logger.warn(`impact-analyzer: uses failed for ${packageName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1215
|
+
}
|
|
1216
|
+
const { risk, reasons } = calculateRisk(packageName, reverseDeps, fromVersion, toVersion);
|
|
1217
|
+
return {
|
|
1218
|
+
packageName,
|
|
1219
|
+
fromVersion,
|
|
1220
|
+
toVersion,
|
|
1221
|
+
packageType,
|
|
1222
|
+
directDeps,
|
|
1223
|
+
reverseDeps,
|
|
1224
|
+
risk,
|
|
1225
|
+
riskReasons: reasons
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1153
1229
|
// src/lib/brew-api.ts
|
|
1154
1230
|
var PKG_PATTERN = /^[\w@./+-]+$/;
|
|
1155
1231
|
function validatePackageName(name) {
|
|
@@ -1157,7 +1233,7 @@ function validatePackageName(name) {
|
|
|
1157
1233
|
}
|
|
1158
1234
|
async function brewUpdate() {
|
|
1159
1235
|
return new Promise((resolve, reject) => {
|
|
1160
|
-
const proc =
|
|
1236
|
+
const proc = spawn("brew", ["update"], { stdio: "ignore" });
|
|
1161
1237
|
proc.on("close", (code) => {
|
|
1162
1238
|
if (code === 0) resolve();
|
|
1163
1239
|
else reject(new Error(`brew update exited with code ${code}`));
|
|
@@ -1247,6 +1323,10 @@ function formulaeToListItems(formulae) {
|
|
|
1247
1323
|
};
|
|
1248
1324
|
});
|
|
1249
1325
|
}
|
|
1326
|
+
async function getUpgradeImpact(packageName, fromVersion, toVersion, packageType) {
|
|
1327
|
+
validatePackageName(packageName);
|
|
1328
|
+
return analyzeUpgradeImpact(packageName, fromVersion, toVersion, packageType);
|
|
1329
|
+
}
|
|
1250
1330
|
function casksToListItems(casks) {
|
|
1251
1331
|
return casks.map((c) => ({
|
|
1252
1332
|
name: c.token,
|
|
@@ -1415,86 +1495,436 @@ var useBrewStore = create4((set, get) => ({
|
|
|
1415
1495
|
}
|
|
1416
1496
|
}));
|
|
1417
1497
|
|
|
1418
|
-
// src/
|
|
1419
|
-
import {
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
const
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1498
|
+
// src/stores/security-store.ts
|
|
1499
|
+
import { create as create5 } from "zustand";
|
|
1500
|
+
|
|
1501
|
+
// src/lib/security/osv-api.ts
|
|
1502
|
+
var OSV_BATCH_URL = "https://api.osv.dev/v1/querybatch";
|
|
1503
|
+
function mapSeverity(vuln) {
|
|
1504
|
+
const dbSev = vuln.database_specific?.severity?.toUpperCase();
|
|
1505
|
+
if (dbSev && ["CRITICAL", "HIGH", "MEDIUM", "LOW"].includes(dbSev)) {
|
|
1506
|
+
return dbSev;
|
|
1507
|
+
}
|
|
1508
|
+
const cvss = vuln.severity?.find((s) => s.type === "CVSS_V3");
|
|
1509
|
+
if (cvss) {
|
|
1510
|
+
const score = parseFloat(cvss.score);
|
|
1511
|
+
if (score >= 9) return "CRITICAL";
|
|
1512
|
+
if (score >= 7) return "HIGH";
|
|
1513
|
+
if (score >= 4) return "MEDIUM";
|
|
1514
|
+
return "LOW";
|
|
1515
|
+
}
|
|
1516
|
+
return "UNKNOWN";
|
|
1517
|
+
}
|
|
1518
|
+
function getFixedVersion(vuln) {
|
|
1519
|
+
for (const affected of vuln.affected ?? []) {
|
|
1520
|
+
for (const range of affected.ranges ?? []) {
|
|
1521
|
+
for (const event of range.events ?? []) {
|
|
1522
|
+
if (event.fixed) return event.fixed;
|
|
1523
|
+
}
|
|
1439
1524
|
}
|
|
1440
|
-
|
|
1525
|
+
}
|
|
1526
|
+
return null;
|
|
1441
1527
|
}
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1528
|
+
var BATCH_SIZE = 100;
|
|
1529
|
+
async function queryBatch(packages, queries) {
|
|
1530
|
+
const res = await fetchWithTimeout(OSV_BATCH_URL, {
|
|
1531
|
+
method: "POST",
|
|
1532
|
+
headers: { "Content-Type": "application/json" },
|
|
1533
|
+
body: JSON.stringify({ queries })
|
|
1534
|
+
}, 15e3);
|
|
1535
|
+
if (!res.ok) {
|
|
1536
|
+
if (res.status === 400 && queries.length > 1) {
|
|
1537
|
+
return queryOneByOne(packages);
|
|
1538
|
+
}
|
|
1539
|
+
throw new Error(`OSV API error: ${res.status} ${res.statusText}`);
|
|
1540
|
+
}
|
|
1541
|
+
const data = await res.json();
|
|
1542
|
+
if (!data || !Array.isArray(data.results)) {
|
|
1543
|
+
throw new Error("Invalid OSV API response: missing results array");
|
|
1544
|
+
}
|
|
1545
|
+
if (data.results.length !== packages.length) {
|
|
1546
|
+
throw new Error(`OSV API response mismatch: expected ${packages.length} results, got ${data.results.length}`);
|
|
1547
|
+
}
|
|
1548
|
+
const result = /* @__PURE__ */ new Map();
|
|
1549
|
+
for (let i = 0; i < packages.length; i++) {
|
|
1550
|
+
const vulns = data.results[i]?.vulns;
|
|
1551
|
+
if (!vulns || vulns.length === 0) continue;
|
|
1552
|
+
result.set(
|
|
1553
|
+
packages[i].name,
|
|
1554
|
+
vulns.map((v) => ({
|
|
1555
|
+
id: v.id,
|
|
1556
|
+
summary: v.summary ?? "No description available",
|
|
1557
|
+
severity: mapSeverity(v),
|
|
1558
|
+
fixedVersion: getFixedVersion(v),
|
|
1559
|
+
references: v.references?.map((r) => r.url) ?? []
|
|
1560
|
+
}))
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1563
|
+
return result;
|
|
1450
1564
|
}
|
|
1451
|
-
function
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1565
|
+
async function queryOneByOne(packages) {
|
|
1566
|
+
const result = /* @__PURE__ */ new Map();
|
|
1567
|
+
const errors = [];
|
|
1568
|
+
for (const pkg of packages) {
|
|
1569
|
+
try {
|
|
1570
|
+
const partial = await queryBatch(
|
|
1571
|
+
[pkg],
|
|
1572
|
+
[{ package: { name: pkg.name, ecosystem: "Homebrew" }, version: pkg.version }]
|
|
1573
|
+
);
|
|
1574
|
+
for (const [k, v] of partial) result.set(k, v);
|
|
1575
|
+
} catch (err) {
|
|
1576
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1577
|
+
if (msg.includes("400")) {
|
|
1578
|
+
errors.push(`Skipped ${pkg.name}: ${msg}`);
|
|
1579
|
+
continue;
|
|
1580
|
+
}
|
|
1581
|
+
if (msg.includes("429")) {
|
|
1582
|
+
logger.warn(`Rate limited by OSV API, backing off`, { package: pkg.name });
|
|
1583
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
1584
|
+
try {
|
|
1585
|
+
const retryResult = await queryBatch(
|
|
1586
|
+
[pkg],
|
|
1587
|
+
[{ package: { name: pkg.name, ecosystem: "Homebrew" }, version: pkg.version }]
|
|
1588
|
+
);
|
|
1589
|
+
for (const [k, v] of retryResult) result.set(k, v);
|
|
1590
|
+
} catch {
|
|
1591
|
+
errors.push(`Failed after retry ${pkg.name}: ${msg}`);
|
|
1592
|
+
}
|
|
1593
|
+
continue;
|
|
1594
|
+
}
|
|
1595
|
+
logger.error(`OSV query failed for ${pkg.name}: ${msg}`);
|
|
1596
|
+
errors.push(`${pkg.name}: ${msg}`);
|
|
1597
|
+
}
|
|
1598
|
+
await new Promise((r) => setTimeout(r, 75));
|
|
1599
|
+
}
|
|
1600
|
+
if (errors.length > 0) {
|
|
1601
|
+
logger.warn(`OSV query errors for ${errors.length} packages`, { errors: errors.slice(0, 5) });
|
|
1602
|
+
}
|
|
1603
|
+
return result;
|
|
1461
1604
|
}
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
};
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
" ",
|
|
1478
|
-
label
|
|
1479
|
-
] });
|
|
1605
|
+
async function queryVulnerabilities(packages) {
|
|
1606
|
+
const validPackages = packages.filter(
|
|
1607
|
+
(p) => p.version && typeof p.version === "string" && p.version.trim().length > 0
|
|
1608
|
+
);
|
|
1609
|
+
const result = /* @__PURE__ */ new Map();
|
|
1610
|
+
for (let i = 0; i < validPackages.length; i += BATCH_SIZE) {
|
|
1611
|
+
const batch = validPackages.slice(i, i + BATCH_SIZE);
|
|
1612
|
+
const queries = batch.map((p) => ({
|
|
1613
|
+
package: { name: p.name, ecosystem: "Homebrew" },
|
|
1614
|
+
version: p.version
|
|
1615
|
+
}));
|
|
1616
|
+
const partial = await queryBatch(batch, queries);
|
|
1617
|
+
for (const [k, v] of partial) result.set(k, v);
|
|
1618
|
+
}
|
|
1619
|
+
return result;
|
|
1480
1620
|
}
|
|
1481
1621
|
|
|
1482
|
-
// src/
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1622
|
+
// src/lib/security/audit-runner.ts
|
|
1623
|
+
var SEVERITY_ORDER = {
|
|
1624
|
+
CRITICAL: 4,
|
|
1625
|
+
HIGH: 3,
|
|
1626
|
+
MEDIUM: 2,
|
|
1627
|
+
LOW: 1,
|
|
1628
|
+
UNKNOWN: 0
|
|
1629
|
+
};
|
|
1630
|
+
async function runSecurityAudit(isPro, formulae, casks) {
|
|
1631
|
+
if (!isPro) throw new Error("Pro license required");
|
|
1632
|
+
const packages = [];
|
|
1633
|
+
for (const f of formulae) {
|
|
1634
|
+
const version = f.installed[0]?.version ?? f.versions.stable;
|
|
1635
|
+
packages.push({ name: f.name, version });
|
|
1636
|
+
}
|
|
1637
|
+
for (const c of casks) {
|
|
1638
|
+
if (c.installed) {
|
|
1639
|
+
packages.push({ name: c.token, version: c.installed });
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
const vulnMap = await queryVulnerabilities(packages);
|
|
1643
|
+
const results = [];
|
|
1644
|
+
let criticalCount = 0;
|
|
1645
|
+
let highCount = 0;
|
|
1646
|
+
let mediumCount = 0;
|
|
1647
|
+
let lowCount = 0;
|
|
1648
|
+
for (const [name, vulns] of vulnMap) {
|
|
1649
|
+
const pkg = packages.find((p) => p.name === name);
|
|
1650
|
+
if (!pkg) continue;
|
|
1651
|
+
const maxSeverity = vulns.reduce(
|
|
1652
|
+
(max, v) => SEVERITY_ORDER[v.severity] > SEVERITY_ORDER[max] ? v.severity : max,
|
|
1653
|
+
"UNKNOWN"
|
|
1654
|
+
);
|
|
1655
|
+
results.push({
|
|
1656
|
+
packageName: name,
|
|
1657
|
+
installedVersion: pkg.version,
|
|
1658
|
+
vulnerabilities: vulns,
|
|
1659
|
+
maxSeverity
|
|
1660
|
+
});
|
|
1661
|
+
for (const v of vulns) {
|
|
1662
|
+
if (v.severity === "CRITICAL") criticalCount++;
|
|
1663
|
+
else if (v.severity === "HIGH") highCount++;
|
|
1664
|
+
else if (v.severity === "MEDIUM") mediumCount++;
|
|
1665
|
+
else if (v.severity === "LOW") lowCount++;
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
results.sort((a, b) => SEVERITY_ORDER[b.maxSeverity] - SEVERITY_ORDER[a.maxSeverity]);
|
|
1669
|
+
return {
|
|
1670
|
+
scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1671
|
+
totalPackages: packages.length,
|
|
1672
|
+
vulnerablePackages: results.length,
|
|
1673
|
+
criticalCount,
|
|
1674
|
+
highCount,
|
|
1675
|
+
mediumCount,
|
|
1676
|
+
lowCount,
|
|
1677
|
+
results
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// src/stores/security-store.ts
|
|
1682
|
+
var CACHE_TTL_MS = 30 * 60 * 1e3;
|
|
1683
|
+
var useSecurityStore = create5((set, get) => ({
|
|
1684
|
+
summary: null,
|
|
1685
|
+
loading: false,
|
|
1686
|
+
error: null,
|
|
1687
|
+
cachedAt: null,
|
|
1688
|
+
scan: async (forceRefresh = false) => {
|
|
1689
|
+
const { summary, cachedAt } = get();
|
|
1690
|
+
if (!forceRefresh && summary && cachedAt && Date.now() - cachedAt < CACHE_TTL_MS) {
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
set({ loading: true, error: null });
|
|
1694
|
+
try {
|
|
1695
|
+
const brewState = useBrewStore.getState();
|
|
1696
|
+
if (brewState.formulae.length === 0) {
|
|
1697
|
+
await brewState.fetchInstalled();
|
|
1698
|
+
}
|
|
1699
|
+
const { formulae, casks } = useBrewStore.getState();
|
|
1700
|
+
const isPro = useLicenseStore.getState().isPro();
|
|
1701
|
+
const result = await runSecurityAudit(isPro, formulae, casks);
|
|
1702
|
+
set({ summary: result, loading: false, cachedAt: Date.now() });
|
|
1703
|
+
} catch (err) {
|
|
1704
|
+
set({ error: err instanceof Error ? err.message : String(err), loading: false });
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
}));
|
|
1708
|
+
|
|
1709
|
+
// src/stores/brewfile-store.ts
|
|
1710
|
+
import { create as create6 } from "zustand";
|
|
1711
|
+
var useBrewfileStore = create6((set, get) => ({
|
|
1712
|
+
schema: null,
|
|
1713
|
+
drift: null,
|
|
1714
|
+
loading: false,
|
|
1715
|
+
driftLoading: false,
|
|
1716
|
+
error: null,
|
|
1717
|
+
load: async () => {
|
|
1718
|
+
set({ loading: true, error: null });
|
|
1719
|
+
try {
|
|
1720
|
+
const schema = await loadBrewfile();
|
|
1721
|
+
set({ schema, loading: false });
|
|
1722
|
+
if (schema) {
|
|
1723
|
+
void get().refreshDrift();
|
|
1724
|
+
}
|
|
1725
|
+
} catch (err) {
|
|
1726
|
+
set({ error: err instanceof Error ? err.message : String(err), loading: false });
|
|
1727
|
+
}
|
|
1728
|
+
},
|
|
1729
|
+
save: async (schema) => {
|
|
1730
|
+
set({ error: null });
|
|
1731
|
+
try {
|
|
1732
|
+
await saveBrewfile(schema);
|
|
1733
|
+
set({ schema });
|
|
1734
|
+
} catch (err) {
|
|
1735
|
+
set({ error: err instanceof Error ? err.message : String(err) });
|
|
1736
|
+
}
|
|
1737
|
+
},
|
|
1738
|
+
refreshDrift: async () => {
|
|
1739
|
+
const { schema } = get();
|
|
1740
|
+
if (!schema) return;
|
|
1741
|
+
set({ driftLoading: true, error: null });
|
|
1742
|
+
try {
|
|
1743
|
+
const drift = await computeDrift(schema);
|
|
1744
|
+
set({ drift, driftLoading: false });
|
|
1745
|
+
} catch (err) {
|
|
1746
|
+
set({ error: err instanceof Error ? err.message : String(err), driftLoading: false });
|
|
1747
|
+
}
|
|
1748
|
+
},
|
|
1749
|
+
createFromCurrent: async (name) => {
|
|
1750
|
+
set({ loading: true, error: null });
|
|
1751
|
+
try {
|
|
1752
|
+
const schema = await createDefaultBrewfile(name);
|
|
1753
|
+
await saveBrewfile(schema);
|
|
1754
|
+
set({ schema, loading: false });
|
|
1755
|
+
void get().refreshDrift();
|
|
1756
|
+
} catch (err) {
|
|
1757
|
+
set({ error: err instanceof Error ? err.message : String(err), loading: false });
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
}));
|
|
1761
|
+
|
|
1762
|
+
// src/stores/sync-store.ts
|
|
1763
|
+
import { create as create7 } from "zustand";
|
|
1764
|
+
var useSyncStore = create7((set, get) => ({
|
|
1765
|
+
config: null,
|
|
1766
|
+
lastResult: null,
|
|
1767
|
+
conflicts: [],
|
|
1768
|
+
loading: false,
|
|
1769
|
+
error: null,
|
|
1770
|
+
initialize: async (isPro) => {
|
|
1771
|
+
if (!isPro) return;
|
|
1772
|
+
try {
|
|
1773
|
+
const config = await loadSyncConfig();
|
|
1774
|
+
set({ config });
|
|
1775
|
+
} catch (err) {
|
|
1776
|
+
logger.warn("sync-store: could not load config", { error: String(err) });
|
|
1777
|
+
}
|
|
1778
|
+
},
|
|
1779
|
+
syncNow: async (isPro, brewfile) => {
|
|
1780
|
+
set({ loading: true, error: null });
|
|
1781
|
+
try {
|
|
1782
|
+
const result = await sync(isPro, brewfile);
|
|
1783
|
+
if (result.conflicts.length > 0) {
|
|
1784
|
+
set({ conflicts: result.conflicts, lastResult: result, loading: false });
|
|
1785
|
+
} else {
|
|
1786
|
+
const config = await loadSyncConfig();
|
|
1787
|
+
set({ config, lastResult: result, conflicts: [], loading: false });
|
|
1788
|
+
}
|
|
1789
|
+
} catch (err) {
|
|
1790
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
1791
|
+
set({ error, loading: false });
|
|
1792
|
+
}
|
|
1793
|
+
},
|
|
1794
|
+
resolveConflicts: async (resolutions) => {
|
|
1795
|
+
set({ loading: true, error: null });
|
|
1796
|
+
try {
|
|
1797
|
+
const config = get().config;
|
|
1798
|
+
if (!config) throw new Error("Sync not initialized");
|
|
1799
|
+
const envelope = await readSyncEnvelope();
|
|
1800
|
+
if (!envelope) throw new Error("No sync data found");
|
|
1801
|
+
const payload = decryptPayload(envelope.encrypted, envelope.iv, envelope.tag);
|
|
1802
|
+
await applyConflictResolutions(payload, resolutions, config.machineId);
|
|
1803
|
+
const updatedConfig = await loadSyncConfig();
|
|
1804
|
+
set({ config: updatedConfig, conflicts: [], loading: false });
|
|
1805
|
+
} catch (err) {
|
|
1806
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
1807
|
+
set({ error, loading: false });
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
}));
|
|
1811
|
+
|
|
1812
|
+
// src/stores/compliance-store.ts
|
|
1813
|
+
import { create as create8 } from "zustand";
|
|
1814
|
+
var useComplianceStore = create8((set, get) => ({
|
|
1815
|
+
policy: null,
|
|
1816
|
+
report: null,
|
|
1817
|
+
loading: false,
|
|
1818
|
+
error: null,
|
|
1819
|
+
importPolicy: async (filePath, isPro) => {
|
|
1820
|
+
if (!isPro) {
|
|
1821
|
+
set({ error: "Pro license required" });
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1824
|
+
set({ loading: true, error: null });
|
|
1825
|
+
try {
|
|
1826
|
+
const policy = await loadPolicy(filePath);
|
|
1827
|
+
set({ policy, loading: false });
|
|
1828
|
+
} catch (err) {
|
|
1829
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
1830
|
+
set({ error, loading: false });
|
|
1831
|
+
}
|
|
1832
|
+
},
|
|
1833
|
+
runCheck: async (isPro) => {
|
|
1834
|
+
const { policy } = get();
|
|
1835
|
+
if (!policy) return;
|
|
1836
|
+
set({ loading: true, error: null });
|
|
1837
|
+
try {
|
|
1838
|
+
const report = await checkCompliance(policy, isPro);
|
|
1839
|
+
set({ report, loading: false });
|
|
1840
|
+
} catch (err) {
|
|
1841
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
1842
|
+
set({ error, loading: false });
|
|
1843
|
+
}
|
|
1844
|
+
},
|
|
1845
|
+
clearPolicy: () => set({ policy: null, report: null, error: null })
|
|
1846
|
+
}));
|
|
1847
|
+
|
|
1848
|
+
// src/components/common/stat-card.tsx
|
|
1849
|
+
import { Box as Box5, Text as Text5, useStdout as useStdout2 } from "ink";
|
|
1850
|
+
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1851
|
+
function StatCard({ label, value, color = "white" }) {
|
|
1852
|
+
const { stdout } = useStdout2();
|
|
1853
|
+
const cols = stdout?.columns ?? 80;
|
|
1854
|
+
const minW = cols < 60 ? 12 : cols < 100 ? 14 : 16;
|
|
1855
|
+
return /* @__PURE__ */ jsxs5(
|
|
1856
|
+
Box5,
|
|
1857
|
+
{
|
|
1858
|
+
borderStyle: "round",
|
|
1859
|
+
borderColor: color,
|
|
1860
|
+
paddingX: 2,
|
|
1861
|
+
paddingY: 0,
|
|
1862
|
+
flexDirection: "column",
|
|
1863
|
+
alignItems: "center",
|
|
1864
|
+
minWidth: minW,
|
|
1865
|
+
children: [
|
|
1866
|
+
/* @__PURE__ */ jsx6(Text5, { bold: true, color, children: value }),
|
|
1867
|
+
/* @__PURE__ */ jsx6(Text5, { color: COLORS.muted, children: label })
|
|
1868
|
+
]
|
|
1869
|
+
}
|
|
1870
|
+
);
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
// src/components/common/loading.tsx
|
|
1874
|
+
import { Box as Box6, Text as Text6 } from "ink";
|
|
1875
|
+
import { Spinner } from "@inkjs/ui";
|
|
1876
|
+
import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1877
|
+
function Loading({ message }) {
|
|
1878
|
+
useLocaleStore((s) => s.locale);
|
|
1879
|
+
return /* @__PURE__ */ jsx7(Box6, { paddingY: 1, children: /* @__PURE__ */ jsx7(Spinner, { label: message ?? t("loading_default") }) });
|
|
1880
|
+
}
|
|
1881
|
+
function ErrorMessage({ message }) {
|
|
1882
|
+
useLocaleStore((s) => s.locale);
|
|
1883
|
+
return /* @__PURE__ */ jsxs6(Box6, { paddingY: 1, children: [
|
|
1884
|
+
/* @__PURE__ */ jsxs6(Text6, { color: COLORS.error, bold: true, children: [
|
|
1885
|
+
"\u2718",
|
|
1886
|
+
" ",
|
|
1887
|
+
t("error_prefix")
|
|
1888
|
+
] }),
|
|
1889
|
+
/* @__PURE__ */ jsx7(Text6, { color: COLORS.error, children: message })
|
|
1890
|
+
] });
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
// src/components/common/status-badge.tsx
|
|
1894
|
+
import { Text as Text7 } from "ink";
|
|
1895
|
+
import { jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1896
|
+
var BADGE_STYLES = {
|
|
1897
|
+
success: { icon: "\u2714", color: COLORS.success },
|
|
1898
|
+
warning: { icon: "\u25B2", color: COLORS.warning },
|
|
1899
|
+
error: { icon: "\u2718", color: COLORS.error },
|
|
1900
|
+
info: { icon: "\u25C6", color: COLORS.blue },
|
|
1901
|
+
muted: { icon: "\u25CB", color: COLORS.textSecondary }
|
|
1902
|
+
};
|
|
1903
|
+
function StatusBadge({ label, variant }) {
|
|
1904
|
+
const { icon, color } = BADGE_STYLES[variant];
|
|
1905
|
+
return /* @__PURE__ */ jsxs7(Text7, { color, children: [
|
|
1906
|
+
icon,
|
|
1907
|
+
" ",
|
|
1908
|
+
label
|
|
1909
|
+
] });
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
// src/components/common/section-header.tsx
|
|
1913
|
+
import { Box as Box7, Text as Text8 } from "ink";
|
|
1914
|
+
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1915
|
+
function SectionHeader({ emoji, title, color = COLORS.gold, gradient, count }) {
|
|
1916
|
+
return /* @__PURE__ */ jsxs8(Box7, { gap: 1, children: [
|
|
1917
|
+
/* @__PURE__ */ jsxs8(Text8, { children: [
|
|
1918
|
+
emoji,
|
|
1919
|
+
" "
|
|
1920
|
+
] }),
|
|
1921
|
+
gradient ? /* @__PURE__ */ jsx8(GradientText, { colors: gradient, bold: true, children: title }) : /* @__PURE__ */ jsx8(Text8, { bold: true, color, children: title }),
|
|
1922
|
+
count !== void 0 && /* @__PURE__ */ jsxs8(Text8, { color: COLORS.textSecondary, children: [
|
|
1923
|
+
"(",
|
|
1924
|
+
count,
|
|
1925
|
+
")"
|
|
1926
|
+
] })
|
|
1927
|
+
] });
|
|
1498
1928
|
}
|
|
1499
1929
|
|
|
1500
1930
|
// src/components/common/version-arrow.tsx
|
|
@@ -1547,8 +1977,46 @@ function truncate(str, maxLen) {
|
|
|
1547
1977
|
|
|
1548
1978
|
// src/views/dashboard.tsx
|
|
1549
1979
|
import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1980
|
+
function ProStatusPanel() {
|
|
1981
|
+
const security = useSecurityStore((s) => s.summary);
|
|
1982
|
+
const drift = useBrewfileStore((s) => s.drift);
|
|
1983
|
+
const syncConfig = useSyncStore((s) => s.config);
|
|
1984
|
+
const complianceReport = useComplianceStore((s) => s.report);
|
|
1985
|
+
const cveCount = security ? security.vulnerablePackages : null;
|
|
1986
|
+
const criticalCount = security ? security.criticalCount : null;
|
|
1987
|
+
const driftScore = drift ? drift.score : null;
|
|
1988
|
+
const lastSync = syncConfig?.lastSync ?? null;
|
|
1989
|
+
const syncAgo = lastSync ? formatRelativeTime(new Date(lastSync).getTime() / 1e3) : null;
|
|
1990
|
+
const violationCount = complianceReport ? complianceReport.violations.length : null;
|
|
1991
|
+
return /* @__PURE__ */ jsxs10(Box8, { flexDirection: "column", borderStyle: "round", borderColor: COLORS.purple, paddingX: 2, paddingY: 0, marginTop: 1, children: [
|
|
1992
|
+
/* @__PURE__ */ jsx10(Text10, { bold: true, color: COLORS.purple, children: t("dashboard_pro_status") }),
|
|
1993
|
+
/* @__PURE__ */ jsxs10(Box8, { gap: 1, children: [
|
|
1994
|
+
/* @__PURE__ */ jsx10(Text10, { color: COLORS.muted, children: t("dashboard_security") }),
|
|
1995
|
+
cveCount === null ? /* @__PURE__ */ jsx10(Text10, { color: COLORS.muted, children: "\u2014" }) : cveCount === 0 ? /* @__PURE__ */ jsx10(Text10, { color: COLORS.success, children: t("dashboard_no_cves") }) : /* @__PURE__ */ jsxs10(Text10, { color: COLORS.error, children: [
|
|
1996
|
+
t("dashboard_cves", { count: String(cveCount) }),
|
|
1997
|
+
criticalCount && criticalCount > 0 ? ` (${criticalCount} critical)` : ""
|
|
1998
|
+
] })
|
|
1999
|
+
] }),
|
|
2000
|
+
/* @__PURE__ */ jsxs10(Box8, { gap: 1, children: [
|
|
2001
|
+
/* @__PURE__ */ jsx10(Text10, { color: COLORS.muted, children: t("dashboard_brewfile") }),
|
|
2002
|
+
driftScore === null ? /* @__PURE__ */ jsx10(Text10, { color: COLORS.muted, children: "\u2014" }) : /* @__PURE__ */ jsxs10(Text10, { color: driftScore >= 80 ? COLORS.success : COLORS.warning, children: [
|
|
2003
|
+
driftScore,
|
|
2004
|
+
"%"
|
|
2005
|
+
] })
|
|
2006
|
+
] }),
|
|
2007
|
+
/* @__PURE__ */ jsxs10(Box8, { gap: 1, children: [
|
|
2008
|
+
/* @__PURE__ */ jsx10(Text10, { color: COLORS.muted, children: t("dashboard_sync") }),
|
|
2009
|
+
syncAgo === null ? /* @__PURE__ */ jsx10(Text10, { color: COLORS.muted, children: t("dashboard_sync_never") }) : /* @__PURE__ */ jsx10(Text10, { color: COLORS.info, children: t("dashboard_sync_ago", { time: syncAgo }) })
|
|
2010
|
+
] }),
|
|
2011
|
+
/* @__PURE__ */ jsxs10(Box8, { gap: 1, children: [
|
|
2012
|
+
/* @__PURE__ */ jsx10(Text10, { color: COLORS.muted, children: t("dashboard_compliance") }),
|
|
2013
|
+
violationCount === null ? /* @__PURE__ */ jsx10(Text10, { color: COLORS.muted, children: "\u2014" }) : violationCount === 0 ? /* @__PURE__ */ jsx10(Text10, { color: COLORS.success, children: t("dashboard_compliance_ok") }) : /* @__PURE__ */ jsx10(Text10, { color: COLORS.warning, children: t("dashboard_compliance_violations", { count: String(violationCount) }) })
|
|
2014
|
+
] })
|
|
2015
|
+
] });
|
|
2016
|
+
}
|
|
1550
2017
|
function DashboardView() {
|
|
1551
2018
|
const { formulae, casks, outdated, services, config, loading, errors, lastFetchedAt, fetchAll } = useBrewStore();
|
|
2019
|
+
const isPro = useLicenseStore((s) => s.isPro);
|
|
1552
2020
|
const { stdout } = useStdout3();
|
|
1553
2021
|
const columns = stdout?.columns ?? 80;
|
|
1554
2022
|
useEffect(() => {
|
|
@@ -1642,7 +2110,8 @@ function DashboardView() {
|
|
|
1642
2110
|
/* @__PURE__ */ jsx10(Text10, { children: s.name }),
|
|
1643
2111
|
s.exit_code != null && /* @__PURE__ */ jsx10(Text10, { color: COLORS.muted, children: t("common_exit", { code: s.exit_code }) })
|
|
1644
2112
|
] }, s.name)) })
|
|
1645
|
-
] })
|
|
2113
|
+
] }),
|
|
2114
|
+
isPro() && /* @__PURE__ */ jsx10(ProStatusPanel, {})
|
|
1646
2115
|
] });
|
|
1647
2116
|
}
|
|
1648
2117
|
|
|
@@ -1669,7 +2138,7 @@ async function logToHistory(args, success, error) {
|
|
|
1669
2138
|
if (!detected) return;
|
|
1670
2139
|
try {
|
|
1671
2140
|
const isPro = useLicenseStore.getState().isPro();
|
|
1672
|
-
const { appendEntry: appendEntry2 } = await import("./history-logger-
|
|
2141
|
+
const { appendEntry: appendEntry2 } = await import("./history-logger-PBDOLKNJ.js");
|
|
1673
2142
|
await appendEntry2(isPro, detected.action, detected.packageName, success, error);
|
|
1674
2143
|
} catch {
|
|
1675
2144
|
}
|
|
@@ -1719,6 +2188,13 @@ function useBrewStream() {
|
|
|
1719
2188
|
if (!cancelRef.current) {
|
|
1720
2189
|
void logToHistory(args, streamError === null, streamError);
|
|
1721
2190
|
}
|
|
2191
|
+
const MUTATING_COMMANDS = /* @__PURE__ */ new Set(["install", "uninstall", "upgrade", "pin", "unpin", "tap", "untap"]);
|
|
2192
|
+
if (!cancelRef.current && MUTATING_COMMANDS.has(args[0] ?? "")) {
|
|
2193
|
+
void import("./snapshot-RAPGMAJF.js").then(({ captureSnapshot: captureSnapshot2, saveSnapshot: saveSnapshot2 }) => {
|
|
2194
|
+
captureSnapshot2().then((s) => saveSnapshot2(s)).catch(() => {
|
|
2195
|
+
});
|
|
2196
|
+
});
|
|
2197
|
+
}
|
|
1722
2198
|
}, []);
|
|
1723
2199
|
const cancel = useCallback(() => {
|
|
1724
2200
|
cancelRef.current = true;
|
|
@@ -2207,12 +2683,35 @@ function SearchView() {
|
|
|
2207
2683
|
import { useEffect as useEffect7, useRef as useRef3, useState as useState5 } from "react";
|
|
2208
2684
|
import { Box as Box16, Text as Text18, useInput as useInput5, useStdout as useStdout5 } from "ink";
|
|
2209
2685
|
import { jsx as jsx18, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
2686
|
+
function ImpactPanel({ impact }) {
|
|
2687
|
+
const riskColor = impact.risk === "high" ? COLORS.error : impact.risk === "medium" ? COLORS.warning : COLORS.success;
|
|
2688
|
+
const riskLabel = impact.risk === "high" ? t("impact_high") : impact.risk === "medium" ? t("impact_medium") : t("impact_low");
|
|
2689
|
+
const riskIcon = impact.risk === "high" ? "\u26A0" : impact.risk === "medium" ? "~" : "\u2713";
|
|
2690
|
+
return /* @__PURE__ */ jsxs17(Box16, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: riskColor, paddingX: 2, paddingY: 0, children: [
|
|
2691
|
+
/* @__PURE__ */ jsxs17(Box16, { children: [
|
|
2692
|
+
/* @__PURE__ */ jsxs17(Text18, { bold: true, color: riskColor, children: [
|
|
2693
|
+
riskIcon,
|
|
2694
|
+
" ",
|
|
2695
|
+
riskLabel
|
|
2696
|
+
] }),
|
|
2697
|
+
impact.reverseDeps.length > 0 && /* @__PURE__ */ jsxs17(Text18, { color: COLORS.textSecondary, children: [
|
|
2698
|
+
" \\u2014 ",
|
|
2699
|
+
t("impact_affects", { count: impact.reverseDeps.length })
|
|
2700
|
+
] })
|
|
2701
|
+
] }),
|
|
2702
|
+
impact.riskReasons.length > 0 && /* @__PURE__ */ jsx18(Text18, { color: COLORS.textSecondary, children: impact.riskReasons.join(" \xB7 ") }),
|
|
2703
|
+
impact.reverseDeps.length > 0 && impact.reverseDeps.length <= 5 && /* @__PURE__ */ jsx18(Text18, { color: COLORS.muted, dimColor: true, children: t("impact_usedBy", { packages: impact.reverseDeps.join(", ") }) }),
|
|
2704
|
+
impact.risk === "high" && /* @__PURE__ */ jsx18(Text18, { color: COLORS.info, children: t("impact_brewfile_hint") })
|
|
2705
|
+
] });
|
|
2706
|
+
}
|
|
2210
2707
|
function OutdatedView() {
|
|
2211
2708
|
const { outdated, loading, errors, fetchOutdated } = useBrewStore();
|
|
2212
2709
|
const stream = useBrewStream();
|
|
2213
2710
|
const [cursor, setCursor] = useState5(0);
|
|
2214
2711
|
const [confirmAction, setConfirmAction] = useState5(null);
|
|
2215
2712
|
const hasRefreshed = useRef3(false);
|
|
2713
|
+
const [impact, setImpact] = useState5(null);
|
|
2714
|
+
const [impactLoading, setImpactLoading] = useState5(false);
|
|
2216
2715
|
useEffect7(() => {
|
|
2217
2716
|
fetchOutdated();
|
|
2218
2717
|
}, []);
|
|
@@ -2222,7 +2721,25 @@ function OutdatedView() {
|
|
|
2222
2721
|
void fetchOutdated();
|
|
2223
2722
|
}
|
|
2224
2723
|
}, [stream.isRunning, stream.error]);
|
|
2225
|
-
const allOutdated = [
|
|
2724
|
+
const allOutdated = [
|
|
2725
|
+
...outdated.formulae.map((p) => ({ ...p, type: "formula" })),
|
|
2726
|
+
...outdated.casks.map((p) => ({ ...p, type: "cask" }))
|
|
2727
|
+
];
|
|
2728
|
+
const debouncedCursor = useDebounce(cursor, 150);
|
|
2729
|
+
useEffect7(() => {
|
|
2730
|
+
const pkg = allOutdated[debouncedCursor];
|
|
2731
|
+
if (!pkg || stream.isRunning) {
|
|
2732
|
+
setImpact(null);
|
|
2733
|
+
return;
|
|
2734
|
+
}
|
|
2735
|
+
setImpactLoading(true);
|
|
2736
|
+
void getUpgradeImpact(
|
|
2737
|
+
pkg.name,
|
|
2738
|
+
pkg.installed_versions[0] ?? "",
|
|
2739
|
+
pkg.current_version,
|
|
2740
|
+
pkg.type
|
|
2741
|
+
).then(setImpact).catch(() => setImpact(null)).finally(() => setImpactLoading(false));
|
|
2742
|
+
}, [debouncedCursor, stream.isRunning]);
|
|
2226
2743
|
useInput5((input, key) => {
|
|
2227
2744
|
if (stream.isRunning) {
|
|
2228
2745
|
if (key.escape) stream.cancel();
|
|
@@ -2336,7 +2853,10 @@ ${t("outdated_upgradeAllList", { list: allOutdated.map((p) => p.name).join(", ")
|
|
|
2336
2853
|
cursor + 1,
|
|
2337
2854
|
"/",
|
|
2338
2855
|
allOutdated.length
|
|
2339
|
-
] }) })
|
|
2856
|
+
] }) }),
|
|
2857
|
+
impact && !stream.isRunning && !confirmAction && /* @__PURE__ */ jsx18(ImpactPanel, { impact }),
|
|
2858
|
+
impactLoading && !stream.isRunning && !confirmAction && /* @__PURE__ */ jsx18(Box16, { marginTop: 1, children: /* @__PURE__ */ jsx18(Text18, { color: COLORS.textSecondary, children: t("impact_analyzing") }) }),
|
|
2859
|
+
/* @__PURE__ */ jsx18(Box16, { marginTop: 1, children: /* @__PURE__ */ jsx18(Text18, { color: COLORS.textSecondary, children: t("impact_hint") }) })
|
|
2340
2860
|
] })
|
|
2341
2861
|
] });
|
|
2342
2862
|
}
|
|
@@ -2728,7 +3248,7 @@ import { useEffect as useEffect11, useRef as useRef6, useState as useState8 } fr
|
|
|
2728
3248
|
import { Box as Box24, useInput as useInput9 } from "ink";
|
|
2729
3249
|
|
|
2730
3250
|
// src/stores/profile-store.ts
|
|
2731
|
-
import { create as
|
|
3251
|
+
import { create as create9 } from "zustand";
|
|
2732
3252
|
|
|
2733
3253
|
// src/lib/profiles/profile-manager.ts
|
|
2734
3254
|
import { readFile as readFile3, writeFile as writeFile3, readdir, rm as rm2, rename as rename2 } from "fs/promises";
|
|
@@ -2778,7 +3298,7 @@ async function loadProfile(isPro, name) {
|
|
|
2778
3298
|
try {
|
|
2779
3299
|
file = JSON.parse(raw);
|
|
2780
3300
|
} catch (err) {
|
|
2781
|
-
throw new Error(`Profile "${name}" is corrupted: ${err instanceof Error ? err.message : String(err)}
|
|
3301
|
+
throw new Error(`Profile "${name}" is corrupted: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
2782
3302
|
}
|
|
2783
3303
|
if (file.version !== 1) {
|
|
2784
3304
|
throw new Error("Unsupported data version");
|
|
@@ -2894,7 +3414,7 @@ async function* importProfile(isPro, profile) {
|
|
|
2894
3414
|
function getIsPro() {
|
|
2895
3415
|
return useLicenseStore.getState().isPro();
|
|
2896
3416
|
}
|
|
2897
|
-
var useProfileStore =
|
|
3417
|
+
var useProfileStore = create9((set) => ({
|
|
2898
3418
|
profileNames: [],
|
|
2899
3419
|
selectedProfile: null,
|
|
2900
3420
|
loading: false,
|
|
@@ -3284,7 +3804,7 @@ import { useEffect as useEffect12, useRef as useRef7, useState as useState9 } fr
|
|
|
3284
3804
|
import { Box as Box25, Text as Text26, useInput as useInput10 } from "ink";
|
|
3285
3805
|
|
|
3286
3806
|
// src/stores/cleanup-store.ts
|
|
3287
|
-
import { create as
|
|
3807
|
+
import { create as create10 } from "zustand";
|
|
3288
3808
|
|
|
3289
3809
|
// src/lib/cleanup/cleanup-analyzer.ts
|
|
3290
3810
|
import { execFile } from "child_process";
|
|
@@ -3365,7 +3885,7 @@ async function analyzeCleanup(isPro, formulae, leaves) {
|
|
|
3365
3885
|
}
|
|
3366
3886
|
|
|
3367
3887
|
// src/stores/cleanup-store.ts
|
|
3368
|
-
var useCleanupStore =
|
|
3888
|
+
var useCleanupStore = create10((set, get) => ({
|
|
3369
3889
|
summary: null,
|
|
3370
3890
|
selected: /* @__PURE__ */ new Set(),
|
|
3371
3891
|
loading: false,
|
|
@@ -3539,8 +4059,8 @@ import { useEffect as useEffect13, useState as useState10, useMemo as useMemo4 }
|
|
|
3539
4059
|
import { Box as Box26, Text as Text27, useInput as useInput11, useStdout as useStdout7 } from "ink";
|
|
3540
4060
|
|
|
3541
4061
|
// src/stores/history-store.ts
|
|
3542
|
-
import { create as
|
|
3543
|
-
var useHistoryStore =
|
|
4062
|
+
import { create as create11 } from "zustand";
|
|
4063
|
+
var useHistoryStore = create11((set) => ({
|
|
3544
4064
|
entries: [],
|
|
3545
4065
|
loading: false,
|
|
3546
4066
|
error: null,
|
|
@@ -3733,219 +4253,6 @@ function HistoryView() {
|
|
|
3733
4253
|
// src/views/security-audit.tsx
|
|
3734
4254
|
import { useEffect as useEffect14, useState as useState11 } from "react";
|
|
3735
4255
|
import { Box as Box27, Text as Text28, useInput as useInput12 } from "ink";
|
|
3736
|
-
|
|
3737
|
-
// src/stores/security-store.ts
|
|
3738
|
-
import { create as create8 } from "zustand";
|
|
3739
|
-
|
|
3740
|
-
// src/lib/security/osv-api.ts
|
|
3741
|
-
var OSV_BATCH_URL = "https://api.osv.dev/v1/querybatch";
|
|
3742
|
-
function mapSeverity(vuln) {
|
|
3743
|
-
const dbSev = vuln.database_specific?.severity?.toUpperCase();
|
|
3744
|
-
if (dbSev && ["CRITICAL", "HIGH", "MEDIUM", "LOW"].includes(dbSev)) {
|
|
3745
|
-
return dbSev;
|
|
3746
|
-
}
|
|
3747
|
-
const cvss = vuln.severity?.find((s) => s.type === "CVSS_V3");
|
|
3748
|
-
if (cvss) {
|
|
3749
|
-
const score = parseFloat(cvss.score);
|
|
3750
|
-
if (score >= 9) return "CRITICAL";
|
|
3751
|
-
if (score >= 7) return "HIGH";
|
|
3752
|
-
if (score >= 4) return "MEDIUM";
|
|
3753
|
-
return "LOW";
|
|
3754
|
-
}
|
|
3755
|
-
return "UNKNOWN";
|
|
3756
|
-
}
|
|
3757
|
-
function getFixedVersion(vuln) {
|
|
3758
|
-
for (const affected of vuln.affected ?? []) {
|
|
3759
|
-
for (const range of affected.ranges ?? []) {
|
|
3760
|
-
for (const event of range.events ?? []) {
|
|
3761
|
-
if (event.fixed) return event.fixed;
|
|
3762
|
-
}
|
|
3763
|
-
}
|
|
3764
|
-
}
|
|
3765
|
-
return null;
|
|
3766
|
-
}
|
|
3767
|
-
var BATCH_SIZE = 100;
|
|
3768
|
-
async function queryBatch(packages, queries) {
|
|
3769
|
-
const res = await fetchWithTimeout(OSV_BATCH_URL, {
|
|
3770
|
-
method: "POST",
|
|
3771
|
-
headers: { "Content-Type": "application/json" },
|
|
3772
|
-
body: JSON.stringify({ queries })
|
|
3773
|
-
}, 15e3);
|
|
3774
|
-
if (!res.ok) {
|
|
3775
|
-
if (res.status === 400 && queries.length > 1) {
|
|
3776
|
-
return queryOneByOne(packages);
|
|
3777
|
-
}
|
|
3778
|
-
throw new Error(`OSV API error: ${res.status} ${res.statusText}`);
|
|
3779
|
-
}
|
|
3780
|
-
const data = await res.json();
|
|
3781
|
-
if (!data || !Array.isArray(data.results)) {
|
|
3782
|
-
throw new Error("Invalid OSV API response: missing results array");
|
|
3783
|
-
}
|
|
3784
|
-
if (data.results.length !== packages.length) {
|
|
3785
|
-
throw new Error(`OSV API response mismatch: expected ${packages.length} results, got ${data.results.length}`);
|
|
3786
|
-
}
|
|
3787
|
-
const result = /* @__PURE__ */ new Map();
|
|
3788
|
-
for (let i = 0; i < packages.length; i++) {
|
|
3789
|
-
const vulns = data.results[i]?.vulns;
|
|
3790
|
-
if (!vulns || vulns.length === 0) continue;
|
|
3791
|
-
result.set(
|
|
3792
|
-
packages[i].name,
|
|
3793
|
-
vulns.map((v) => ({
|
|
3794
|
-
id: v.id,
|
|
3795
|
-
summary: v.summary ?? "No description available",
|
|
3796
|
-
severity: mapSeverity(v),
|
|
3797
|
-
fixedVersion: getFixedVersion(v),
|
|
3798
|
-
references: v.references?.map((r) => r.url) ?? []
|
|
3799
|
-
}))
|
|
3800
|
-
);
|
|
3801
|
-
}
|
|
3802
|
-
return result;
|
|
3803
|
-
}
|
|
3804
|
-
async function queryOneByOne(packages) {
|
|
3805
|
-
const result = /* @__PURE__ */ new Map();
|
|
3806
|
-
const errors = [];
|
|
3807
|
-
for (const pkg of packages) {
|
|
3808
|
-
try {
|
|
3809
|
-
const partial = await queryBatch(
|
|
3810
|
-
[pkg],
|
|
3811
|
-
[{ package: { name: pkg.name, ecosystem: "Homebrew" }, version: pkg.version }]
|
|
3812
|
-
);
|
|
3813
|
-
for (const [k, v] of partial) result.set(k, v);
|
|
3814
|
-
} catch (err) {
|
|
3815
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
3816
|
-
if (msg.includes("400")) {
|
|
3817
|
-
errors.push(`Skipped ${pkg.name}: ${msg}`);
|
|
3818
|
-
continue;
|
|
3819
|
-
}
|
|
3820
|
-
if (msg.includes("429")) {
|
|
3821
|
-
logger.warn(`Rate limited by OSV API, backing off`, { package: pkg.name });
|
|
3822
|
-
await new Promise((r) => setTimeout(r, 2e3));
|
|
3823
|
-
try {
|
|
3824
|
-
const retryResult = await queryBatch(
|
|
3825
|
-
[pkg],
|
|
3826
|
-
[{ package: { name: pkg.name, ecosystem: "Homebrew" }, version: pkg.version }]
|
|
3827
|
-
);
|
|
3828
|
-
for (const [k, v] of retryResult) result.set(k, v);
|
|
3829
|
-
} catch {
|
|
3830
|
-
errors.push(`Failed after retry ${pkg.name}: ${msg}`);
|
|
3831
|
-
}
|
|
3832
|
-
continue;
|
|
3833
|
-
}
|
|
3834
|
-
logger.error(`OSV query failed for ${pkg.name}: ${msg}`);
|
|
3835
|
-
errors.push(`${pkg.name}: ${msg}`);
|
|
3836
|
-
}
|
|
3837
|
-
await new Promise((r) => setTimeout(r, 75));
|
|
3838
|
-
}
|
|
3839
|
-
if (errors.length > 0) {
|
|
3840
|
-
logger.warn(`OSV query errors for ${errors.length} packages`, { errors: errors.slice(0, 5) });
|
|
3841
|
-
}
|
|
3842
|
-
return result;
|
|
3843
|
-
}
|
|
3844
|
-
async function queryVulnerabilities(packages) {
|
|
3845
|
-
const validPackages = packages.filter(
|
|
3846
|
-
(p) => p.version && typeof p.version === "string" && p.version.trim().length > 0
|
|
3847
|
-
);
|
|
3848
|
-
const result = /* @__PURE__ */ new Map();
|
|
3849
|
-
for (let i = 0; i < validPackages.length; i += BATCH_SIZE) {
|
|
3850
|
-
const batch = validPackages.slice(i, i + BATCH_SIZE);
|
|
3851
|
-
const queries = batch.map((p) => ({
|
|
3852
|
-
package: { name: p.name, ecosystem: "Homebrew" },
|
|
3853
|
-
version: p.version
|
|
3854
|
-
}));
|
|
3855
|
-
const partial = await queryBatch(batch, queries);
|
|
3856
|
-
for (const [k, v] of partial) result.set(k, v);
|
|
3857
|
-
}
|
|
3858
|
-
return result;
|
|
3859
|
-
}
|
|
3860
|
-
|
|
3861
|
-
// src/lib/security/audit-runner.ts
|
|
3862
|
-
var SEVERITY_ORDER = {
|
|
3863
|
-
CRITICAL: 4,
|
|
3864
|
-
HIGH: 3,
|
|
3865
|
-
MEDIUM: 2,
|
|
3866
|
-
LOW: 1,
|
|
3867
|
-
UNKNOWN: 0
|
|
3868
|
-
};
|
|
3869
|
-
async function runSecurityAudit(isPro, formulae, casks) {
|
|
3870
|
-
if (!isPro) throw new Error("Pro license required");
|
|
3871
|
-
const packages = [];
|
|
3872
|
-
for (const f of formulae) {
|
|
3873
|
-
const version = f.installed[0]?.version ?? f.versions.stable;
|
|
3874
|
-
packages.push({ name: f.name, version });
|
|
3875
|
-
}
|
|
3876
|
-
for (const c of casks) {
|
|
3877
|
-
if (c.installed) {
|
|
3878
|
-
packages.push({ name: c.token, version: c.installed });
|
|
3879
|
-
}
|
|
3880
|
-
}
|
|
3881
|
-
const vulnMap = await queryVulnerabilities(packages);
|
|
3882
|
-
const results = [];
|
|
3883
|
-
let criticalCount = 0;
|
|
3884
|
-
let highCount = 0;
|
|
3885
|
-
let mediumCount = 0;
|
|
3886
|
-
let lowCount = 0;
|
|
3887
|
-
for (const [name, vulns] of vulnMap) {
|
|
3888
|
-
const pkg = packages.find((p) => p.name === name);
|
|
3889
|
-
if (!pkg) continue;
|
|
3890
|
-
const maxSeverity = vulns.reduce(
|
|
3891
|
-
(max, v) => SEVERITY_ORDER[v.severity] > SEVERITY_ORDER[max] ? v.severity : max,
|
|
3892
|
-
"UNKNOWN"
|
|
3893
|
-
);
|
|
3894
|
-
results.push({
|
|
3895
|
-
packageName: name,
|
|
3896
|
-
installedVersion: pkg.version,
|
|
3897
|
-
vulnerabilities: vulns,
|
|
3898
|
-
maxSeverity
|
|
3899
|
-
});
|
|
3900
|
-
for (const v of vulns) {
|
|
3901
|
-
if (v.severity === "CRITICAL") criticalCount++;
|
|
3902
|
-
else if (v.severity === "HIGH") highCount++;
|
|
3903
|
-
else if (v.severity === "MEDIUM") mediumCount++;
|
|
3904
|
-
else if (v.severity === "LOW") lowCount++;
|
|
3905
|
-
}
|
|
3906
|
-
}
|
|
3907
|
-
results.sort((a, b) => SEVERITY_ORDER[b.maxSeverity] - SEVERITY_ORDER[a.maxSeverity]);
|
|
3908
|
-
return {
|
|
3909
|
-
scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3910
|
-
totalPackages: packages.length,
|
|
3911
|
-
vulnerablePackages: results.length,
|
|
3912
|
-
criticalCount,
|
|
3913
|
-
highCount,
|
|
3914
|
-
mediumCount,
|
|
3915
|
-
lowCount,
|
|
3916
|
-
results
|
|
3917
|
-
};
|
|
3918
|
-
}
|
|
3919
|
-
|
|
3920
|
-
// src/stores/security-store.ts
|
|
3921
|
-
var CACHE_TTL_MS = 30 * 60 * 1e3;
|
|
3922
|
-
var useSecurityStore = create8((set, get) => ({
|
|
3923
|
-
summary: null,
|
|
3924
|
-
loading: false,
|
|
3925
|
-
error: null,
|
|
3926
|
-
cachedAt: null,
|
|
3927
|
-
scan: async (forceRefresh = false) => {
|
|
3928
|
-
const { summary, cachedAt } = get();
|
|
3929
|
-
if (!forceRefresh && summary && cachedAt && Date.now() - cachedAt < CACHE_TTL_MS) {
|
|
3930
|
-
return;
|
|
3931
|
-
}
|
|
3932
|
-
set({ loading: true, error: null });
|
|
3933
|
-
try {
|
|
3934
|
-
const brewState = useBrewStore.getState();
|
|
3935
|
-
if (brewState.formulae.length === 0) {
|
|
3936
|
-
await brewState.fetchInstalled();
|
|
3937
|
-
}
|
|
3938
|
-
const { formulae, casks } = useBrewStore.getState();
|
|
3939
|
-
const isPro = useLicenseStore.getState().isPro();
|
|
3940
|
-
const result = await runSecurityAudit(isPro, formulae, casks);
|
|
3941
|
-
set({ summary: result, loading: false, cachedAt: Date.now() });
|
|
3942
|
-
} catch (err) {
|
|
3943
|
-
set({ error: err instanceof Error ? err.message : String(err), loading: false });
|
|
3944
|
-
}
|
|
3945
|
-
}
|
|
3946
|
-
}));
|
|
3947
|
-
|
|
3948
|
-
// src/views/security-audit.tsx
|
|
3949
4256
|
import { jsx as jsx29, jsxs as jsxs28 } from "react/jsx-runtime";
|
|
3950
4257
|
var SEVERITY_COLORS = {
|
|
3951
4258
|
CRITICAL: COLORS.error,
|
|
@@ -3966,6 +4273,7 @@ function isNetworkError2(msg) {
|
|
|
3966
4273
|
}
|
|
3967
4274
|
function SecurityAuditView() {
|
|
3968
4275
|
const { summary, loading, error, scan, cachedAt } = useSecurityStore();
|
|
4276
|
+
const navigate = useNavigationStore((s) => s.navigate);
|
|
3969
4277
|
const [cursor, setCursor] = useState11(0);
|
|
3970
4278
|
const [expandedPkg, setExpandedPkg] = useState11(null);
|
|
3971
4279
|
const [confirmUpgrade, setConfirmUpgrade] = useState11(null);
|
|
@@ -3980,6 +4288,10 @@ function SecurityAuditView() {
|
|
|
3980
4288
|
void scan(true);
|
|
3981
4289
|
return;
|
|
3982
4290
|
}
|
|
4291
|
+
if (input === "R") {
|
|
4292
|
+
navigate("rollback");
|
|
4293
|
+
return;
|
|
4294
|
+
}
|
|
3983
4295
|
if (input === "u" && results[cursor]) {
|
|
3984
4296
|
setConfirmUpgrade(results[cursor].packageName);
|
|
3985
4297
|
return;
|
|
@@ -4038,6 +4350,11 @@ function SecurityAuditView() {
|
|
|
4038
4350
|
/* @__PURE__ */ jsx29(Text28, { bold: isCurrent, inverse: isCurrent, color: isCurrent ? COLORS.text : COLORS.muted, children: pkg.packageName }),
|
|
4039
4351
|
/* @__PURE__ */ jsx29(Text28, { color: COLORS.muted, children: pkg.installedVersion }),
|
|
4040
4352
|
/* @__PURE__ */ jsx29(Text28, { color: COLORS.muted, children: tp("plural_vulns", pkg.vulnerabilities.length) }),
|
|
4353
|
+
pkg.vulnerabilities.some((v) => v.fixedVersion) && /* @__PURE__ */ jsxs28(Text28, { color: COLORS.textSecondary, children: [
|
|
4354
|
+
"[R:",
|
|
4355
|
+
t("hint_rollback"),
|
|
4356
|
+
"]"
|
|
4357
|
+
] }),
|
|
4041
4358
|
/* @__PURE__ */ jsx29(Text28, { color: COLORS.muted, children: isExpanded ? "\u25BC" : "\u25B6" })
|
|
4042
4359
|
] }),
|
|
4043
4360
|
isExpanded && /* @__PURE__ */ jsx29(Box27, { flexDirection: "column", paddingLeft: 4, marginBottom: 1, children: pkg.vulnerabilities.map((vuln) => /* @__PURE__ */ jsxs28(Box27, { flexDirection: "column", marginBottom: 1, children: [
|
|
@@ -4058,7 +4375,8 @@ function SecurityAuditView() {
|
|
|
4058
4375
|
cursor + 1,
|
|
4059
4376
|
"/",
|
|
4060
4377
|
results.length
|
|
4061
|
-
] }) })
|
|
4378
|
+
] }) }),
|
|
4379
|
+
/* @__PURE__ */ jsx29(Box27, { marginTop: 1, children: /* @__PURE__ */ jsx29(Text28, { color: COLORS.textSecondary, children: t("security_rollback_hint") }) })
|
|
4062
4380
|
] })
|
|
4063
4381
|
] });
|
|
4064
4382
|
}
|
|
@@ -4279,64 +4597,1253 @@ function AccountView() {
|
|
|
4279
4597
|
/* @__PURE__ */ jsx30(Box28, { marginTop: 1, children: /* @__PURE__ */ jsxs29(Text29, { color: COLORS.textSecondary, children: [
|
|
4280
4598
|
status === "pro" ? `d ${t("hint_deactivate")}` : "",
|
|
4281
4599
|
" ",
|
|
4282
|
-
t("app_version", { version: "0.
|
|
4600
|
+
t("app_version", { version: "0.5.0" })
|
|
4283
4601
|
] }) })
|
|
4284
4602
|
] });
|
|
4285
4603
|
}
|
|
4286
4604
|
|
|
4287
|
-
// src/
|
|
4288
|
-
import {
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
}
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
if (
|
|
4299
|
-
return
|
|
4605
|
+
// src/views/rollback.tsx
|
|
4606
|
+
import { useCallback as useCallback3, useEffect as useEffect15, useRef as useRef8, useState as useState13 } from "react";
|
|
4607
|
+
import { Box as Box29, Text as Text30, useInput as useInput14 } from "ink";
|
|
4608
|
+
|
|
4609
|
+
// src/stores/rollback-store.ts
|
|
4610
|
+
import { create as create12 } from "zustand";
|
|
4611
|
+
|
|
4612
|
+
// src/lib/rollback/rollback-engine.ts
|
|
4613
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
4614
|
+
import { join as join4 } from "path";
|
|
4615
|
+
async function detectStrategy(name, targetVersion, packageType) {
|
|
4616
|
+
if (packageType === "cask") {
|
|
4617
|
+
return { strategy: "pin-only" };
|
|
4300
4618
|
}
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
return /* @__PURE__ */ jsx31(HistoryView, {});
|
|
4322
|
-
case "security-audit":
|
|
4323
|
-
return /* @__PURE__ */ jsx31(SecurityAuditView, {});
|
|
4324
|
-
case "account":
|
|
4325
|
-
return /* @__PURE__ */ jsx31(AccountView, {});
|
|
4619
|
+
try {
|
|
4620
|
+
const major = targetVersion.split(".")[0] ?? "";
|
|
4621
|
+
if (major !== "" && !isNaN(parseInt(major, 10))) {
|
|
4622
|
+
const versionedFormula = `${name}@${major}`;
|
|
4623
|
+
await execBrew(["info", "--json=v2", versionedFormula]);
|
|
4624
|
+
return { strategy: "versioned-formula", versionedFormula };
|
|
4625
|
+
}
|
|
4626
|
+
} catch {
|
|
4627
|
+
}
|
|
4628
|
+
try {
|
|
4629
|
+
const brewCache = (await execBrew(["--cache"])).trim();
|
|
4630
|
+
const downloadsDir = join4(brewCache, "downloads");
|
|
4631
|
+
const entries = await readdir2(downloadsDir);
|
|
4632
|
+
const found = entries.some(
|
|
4633
|
+
(entry) => entry.includes(name) && entry.includes(targetVersion)
|
|
4634
|
+
);
|
|
4635
|
+
if (found) {
|
|
4636
|
+
return { strategy: "bottle-cache" };
|
|
4637
|
+
}
|
|
4638
|
+
} catch {
|
|
4326
4639
|
}
|
|
4640
|
+
return { strategy: "pin-only" };
|
|
4327
4641
|
}
|
|
4328
|
-
function
|
|
4642
|
+
async function buildRollbackPlan(snapshot, isPro) {
|
|
4643
|
+
if (!isPro) throw new Error("Pro license required");
|
|
4644
|
+
const current = await captureSnapshot();
|
|
4645
|
+
const diff = diffSnapshots(snapshot, current);
|
|
4646
|
+
const actions = [];
|
|
4647
|
+
const warnings = [];
|
|
4648
|
+
for (const entry of diff.upgraded) {
|
|
4649
|
+
const type = entry.type;
|
|
4650
|
+
const { strategy, versionedFormula } = await detectStrategy(entry.name, entry.from, type);
|
|
4651
|
+
actions.push({
|
|
4652
|
+
packageName: entry.name,
|
|
4653
|
+
packageType: type,
|
|
4654
|
+
action: "downgrade",
|
|
4655
|
+
fromVersion: entry.to,
|
|
4656
|
+
// current version
|
|
4657
|
+
toVersion: entry.from,
|
|
4658
|
+
// snapshot (target) version
|
|
4659
|
+
strategy,
|
|
4660
|
+
versionedFormula
|
|
4661
|
+
});
|
|
4662
|
+
}
|
|
4663
|
+
for (const entry of diff.downgraded) {
|
|
4664
|
+
const type = entry.type;
|
|
4665
|
+
const { strategy, versionedFormula } = await detectStrategy(entry.name, entry.from, type);
|
|
4666
|
+
actions.push({
|
|
4667
|
+
packageName: entry.name,
|
|
4668
|
+
packageType: type,
|
|
4669
|
+
action: "downgrade",
|
|
4670
|
+
fromVersion: entry.to,
|
|
4671
|
+
// current version
|
|
4672
|
+
toVersion: entry.from,
|
|
4673
|
+
// snapshot (target) version
|
|
4674
|
+
strategy,
|
|
4675
|
+
versionedFormula
|
|
4676
|
+
});
|
|
4677
|
+
}
|
|
4678
|
+
for (const entry of diff.added) {
|
|
4679
|
+
if (entry.type === "tap") continue;
|
|
4680
|
+
const type = entry.type;
|
|
4681
|
+
actions.push({
|
|
4682
|
+
packageName: entry.name,
|
|
4683
|
+
packageType: type,
|
|
4684
|
+
action: "remove",
|
|
4685
|
+
fromVersion: entry.version,
|
|
4686
|
+
toVersion: "",
|
|
4687
|
+
strategy: "unavailable"
|
|
4688
|
+
});
|
|
4689
|
+
}
|
|
4690
|
+
for (const entry of diff.removed) {
|
|
4691
|
+
if (entry.type === "tap") continue;
|
|
4692
|
+
const type = entry.type;
|
|
4693
|
+
const { strategy, versionedFormula } = await detectStrategy(entry.name, entry.version, type);
|
|
4694
|
+
actions.push({
|
|
4695
|
+
packageName: entry.name,
|
|
4696
|
+
packageType: type,
|
|
4697
|
+
action: "install",
|
|
4698
|
+
fromVersion: "",
|
|
4699
|
+
toVersion: entry.version,
|
|
4700
|
+
strategy,
|
|
4701
|
+
versionedFormula
|
|
4702
|
+
});
|
|
4703
|
+
}
|
|
4704
|
+
const caskPinCount = actions.filter(
|
|
4705
|
+
(a) => a.packageType === "cask" && a.action !== "remove" && a.strategy === "pin-only"
|
|
4706
|
+
).length;
|
|
4707
|
+
if (caskPinCount > 0) {
|
|
4708
|
+
warnings.push(`${caskPinCount} cask(s) will be pinned only (version restoration not supported)`);
|
|
4709
|
+
}
|
|
4710
|
+
const canExecute = actions.some((a) => a.strategy !== "unavailable");
|
|
4711
|
+
const snapshotLabel = snapshot.label ?? "Auto";
|
|
4712
|
+
const snapshotDate = new Date(snapshot.capturedAt).toLocaleString();
|
|
4713
|
+
logger.debug("Built rollback plan", { actionCount: actions.length, canExecute });
|
|
4714
|
+
return {
|
|
4715
|
+
snapshotLabel,
|
|
4716
|
+
snapshotDate,
|
|
4717
|
+
actions,
|
|
4718
|
+
warnings,
|
|
4719
|
+
canExecute
|
|
4720
|
+
};
|
|
4721
|
+
}
|
|
4722
|
+
async function* executeRollbackPlan(plan, isPro) {
|
|
4723
|
+
if (!isPro) throw new Error("Pro license required");
|
|
4724
|
+
for (const action of plan.actions) {
|
|
4725
|
+
if (action.strategy === "unavailable") {
|
|
4726
|
+
yield `[skip] ${action.packageName}: cannot restore automatically \u2014 manual action required`;
|
|
4727
|
+
continue;
|
|
4728
|
+
}
|
|
4729
|
+
if (action.action === "remove") {
|
|
4730
|
+
yield `[skip] ${action.packageName}: removal skipped for safety \u2014 remove manually if needed`;
|
|
4731
|
+
continue;
|
|
4732
|
+
}
|
|
4733
|
+
if (action.action === "install") {
|
|
4734
|
+
if (action.strategy === "pin-only") {
|
|
4735
|
+
yield `[warn] ${action.packageName}: cannot install specific version \u2014 skipping`;
|
|
4736
|
+
continue;
|
|
4737
|
+
}
|
|
4738
|
+
if (action.strategy === "versioned-formula" && action.versionedFormula) {
|
|
4739
|
+
yield `[install] ${action.packageName} via ${action.versionedFormula}`;
|
|
4740
|
+
for await (const line of streamBrew(["install", action.versionedFormula])) {
|
|
4741
|
+
yield line;
|
|
4742
|
+
}
|
|
4743
|
+
continue;
|
|
4744
|
+
}
|
|
4745
|
+
if (action.strategy === "bottle-cache") {
|
|
4746
|
+
yield `[install] ${action.packageName} from bottle cache`;
|
|
4747
|
+
for await (const line of streamBrew(["install", "--force-bottle", action.packageName])) {
|
|
4748
|
+
yield line;
|
|
4749
|
+
}
|
|
4750
|
+
continue;
|
|
4751
|
+
}
|
|
4752
|
+
continue;
|
|
4753
|
+
}
|
|
4754
|
+
if (action.strategy === "versioned-formula" && action.versionedFormula) {
|
|
4755
|
+
yield `[downgrade] ${action.packageName}: ${action.fromVersion} \u2192 ${action.toVersion} via ${action.versionedFormula}`;
|
|
4756
|
+
for await (const line of streamBrew(["install", action.versionedFormula])) {
|
|
4757
|
+
yield line;
|
|
4758
|
+
}
|
|
4759
|
+
} else if (action.strategy === "bottle-cache") {
|
|
4760
|
+
yield `[downgrade] ${action.packageName}: ${action.fromVersion} \u2192 ${action.toVersion} from bottle cache`;
|
|
4761
|
+
for await (const line of streamBrew(["install", "--force-bottle", action.packageName])) {
|
|
4762
|
+
yield line;
|
|
4763
|
+
}
|
|
4764
|
+
} else if (action.strategy === "pin-only") {
|
|
4765
|
+
yield `[pin] ${action.packageName}: pinning at current version (target version not restorable)`;
|
|
4766
|
+
await execBrew(["pin", action.packageName]);
|
|
4767
|
+
yield `\u2713 Pinned ${action.packageName}`;
|
|
4768
|
+
}
|
|
4769
|
+
}
|
|
4770
|
+
yield "[snapshot] Capturing post-rollback snapshot...";
|
|
4771
|
+
try {
|
|
4772
|
+
const postSnapshot = await captureSnapshot();
|
|
4773
|
+
await saveSnapshot(postSnapshot, "post-rollback");
|
|
4774
|
+
yield "[snapshot] Snapshot saved.";
|
|
4775
|
+
} catch (err) {
|
|
4776
|
+
logger.warn("Failed to save post-rollback snapshot", { err });
|
|
4777
|
+
yield "[snapshot] Warning: could not save snapshot.";
|
|
4778
|
+
}
|
|
4779
|
+
}
|
|
4780
|
+
|
|
4781
|
+
// src/stores/rollback-store.ts
|
|
4782
|
+
var useRollbackStore = create12((set) => ({
|
|
4783
|
+
snapshots: [],
|
|
4784
|
+
loading: false,
|
|
4785
|
+
error: null,
|
|
4786
|
+
selectedSnapshot: null,
|
|
4787
|
+
plan: null,
|
|
4788
|
+
planLoading: false,
|
|
4789
|
+
planError: null,
|
|
4790
|
+
fetchSnapshots: async (isPro) => {
|
|
4791
|
+
if (!isPro) return;
|
|
4792
|
+
set({ loading: true, error: null });
|
|
4793
|
+
try {
|
|
4794
|
+
const snapshots = await loadSnapshots();
|
|
4795
|
+
set({ snapshots, loading: false });
|
|
4796
|
+
} catch (err) {
|
|
4797
|
+
set({ error: err instanceof Error ? err.message : String(err), loading: false });
|
|
4798
|
+
}
|
|
4799
|
+
},
|
|
4800
|
+
selectSnapshot: async (snapshot, isPro) => {
|
|
4801
|
+
if (!snapshot) {
|
|
4802
|
+
set({ selectedSnapshot: null, plan: null, planError: null });
|
|
4803
|
+
return;
|
|
4804
|
+
}
|
|
4805
|
+
set({ selectedSnapshot: snapshot, plan: null, planLoading: true, planError: null });
|
|
4806
|
+
try {
|
|
4807
|
+
const plan = await buildRollbackPlan(snapshot, isPro);
|
|
4808
|
+
set({ plan, planLoading: false });
|
|
4809
|
+
} catch (err) {
|
|
4810
|
+
set({ planError: err instanceof Error ? err.message : String(err), planLoading: false });
|
|
4811
|
+
}
|
|
4812
|
+
},
|
|
4813
|
+
clearPlan: () => set({ selectedSnapshot: null, plan: null, planError: null })
|
|
4814
|
+
}));
|
|
4815
|
+
|
|
4816
|
+
// src/views/rollback.tsx
|
|
4817
|
+
import { jsx as jsx31, jsxs as jsxs30 } from "react/jsx-runtime";
|
|
4818
|
+
function strategyLabel(action) {
|
|
4819
|
+
switch (action.strategy) {
|
|
4820
|
+
case "versioned-formula":
|
|
4821
|
+
return t("rollback_strategy_versioned");
|
|
4822
|
+
case "bottle-cache":
|
|
4823
|
+
return t("rollback_strategy_bottle");
|
|
4824
|
+
case "pin-only":
|
|
4825
|
+
return t("rollback_strategy_pin");
|
|
4826
|
+
case "unavailable":
|
|
4827
|
+
return t("rollback_strategy_unavailable");
|
|
4828
|
+
}
|
|
4829
|
+
}
|
|
4830
|
+
function actionColor(action) {
|
|
4831
|
+
if (action.strategy === "unavailable") return COLORS.muted;
|
|
4832
|
+
if (action.action === "remove") return COLORS.muted;
|
|
4833
|
+
if (action.action === "downgrade") return COLORS.warning;
|
|
4834
|
+
if (action.action === "install") return COLORS.success;
|
|
4835
|
+
return COLORS.info;
|
|
4836
|
+
}
|
|
4837
|
+
function actionPrefix(action) {
|
|
4838
|
+
if (action.strategy === "unavailable" || action.action === "remove") return "\u2297";
|
|
4839
|
+
if (action.strategy === "pin-only") return "\u{1F4CC}";
|
|
4840
|
+
if (action.action === "downgrade") return "\u2B07";
|
|
4841
|
+
return "\u2B06";
|
|
4842
|
+
}
|
|
4843
|
+
function PlanView({ plan }) {
|
|
4844
|
+
const executableCount = plan.actions.filter((a) => a.strategy !== "unavailable" && a.action !== "remove").length;
|
|
4845
|
+
return /* @__PURE__ */ jsxs30(Box29, { flexDirection: "column", marginTop: 1, children: [
|
|
4846
|
+
/* @__PURE__ */ jsxs30(Box29, { marginBottom: 1, children: [
|
|
4847
|
+
/* @__PURE__ */ jsxs30(Text30, { color: COLORS.text, bold: true, children: [
|
|
4848
|
+
plan.snapshotLabel,
|
|
4849
|
+
" "
|
|
4850
|
+
] }),
|
|
4851
|
+
/* @__PURE__ */ jsx31(Text30, { color: COLORS.textSecondary, children: plan.snapshotDate })
|
|
4852
|
+
] }),
|
|
4853
|
+
plan.actions.length === 0 && /* @__PURE__ */ jsx31(ResultBanner, { status: "success", message: t("rollback_diff_empty") }),
|
|
4854
|
+
plan.actions.map((a) => /* @__PURE__ */ jsxs30(Box29, { children: [
|
|
4855
|
+
/* @__PURE__ */ jsxs30(Text30, { color: actionColor(a), children: [
|
|
4856
|
+
actionPrefix(a),
|
|
4857
|
+
" "
|
|
4858
|
+
] }),
|
|
4859
|
+
/* @__PURE__ */ jsx31(Text30, { color: actionColor(a), bold: true, children: a.packageName }),
|
|
4860
|
+
a.fromVersion !== "" && a.toVersion !== "" && /* @__PURE__ */ jsxs30(Text30, { color: COLORS.textSecondary, children: [
|
|
4861
|
+
" ",
|
|
4862
|
+
a.fromVersion,
|
|
4863
|
+
" \u2192 ",
|
|
4864
|
+
a.toVersion
|
|
4865
|
+
] }),
|
|
4866
|
+
a.fromVersion === "" && a.toVersion !== "" && /* @__PURE__ */ jsxs30(Text30, { color: COLORS.textSecondary, children: [
|
|
4867
|
+
" install ",
|
|
4868
|
+
a.toVersion
|
|
4869
|
+
] }),
|
|
4870
|
+
a.fromVersion !== "" && a.toVersion === "" && /* @__PURE__ */ jsx31(Text30, { color: COLORS.textSecondary, children: " remove" }),
|
|
4871
|
+
/* @__PURE__ */ jsxs30(Text30, { color: COLORS.muted, dimColor: true, children: [
|
|
4872
|
+
" [",
|
|
4873
|
+
strategyLabel(a),
|
|
4874
|
+
"]"
|
|
4875
|
+
] })
|
|
4876
|
+
] }, a.packageName + a.action)),
|
|
4877
|
+
plan.warnings.map((w) => /* @__PURE__ */ jsx31(Box29, { marginTop: 1, children: /* @__PURE__ */ jsxs30(Text30, { color: COLORS.warning, children: [
|
|
4878
|
+
"\u26A0 ",
|
|
4879
|
+
w
|
|
4880
|
+
] }) }, w)),
|
|
4881
|
+
/* @__PURE__ */ jsx31(Box29, { marginTop: 1, children: plan.canExecute ? /* @__PURE__ */ jsxs30(Text30, { color: COLORS.textSecondary, children: [
|
|
4882
|
+
"enter:",
|
|
4883
|
+
t("rollback_confirm", { count: String(executableCount) }),
|
|
4884
|
+
" esc:",
|
|
4885
|
+
t("hint_back")
|
|
4886
|
+
] }) : /* @__PURE__ */ jsxs30(Text30, { color: COLORS.muted, children: [
|
|
4887
|
+
t("rollback_strategy_unavailable"),
|
|
4888
|
+
" esc:",
|
|
4889
|
+
t("hint_back")
|
|
4890
|
+
] }) })
|
|
4891
|
+
] });
|
|
4892
|
+
}
|
|
4893
|
+
function RollbackView() {
|
|
4894
|
+
const isPro = useLicenseStore((s) => s.isPro);
|
|
4895
|
+
const { snapshots, loading, error, plan, planLoading, planError, fetchSnapshots, selectSnapshot, clearPlan } = useRollbackStore();
|
|
4896
|
+
const [cursor, setCursor] = useState13(0);
|
|
4897
|
+
const [phase, setPhase] = useState13("list");
|
|
4898
|
+
const [streamLines, setStreamLines] = useState13([]);
|
|
4899
|
+
const [streamRunning, setStreamRunning] = useState13(false);
|
|
4900
|
+
const [streamError, setStreamError] = useState13(null);
|
|
4901
|
+
const generatorRef = useRef8(null);
|
|
4902
|
+
const mountedRef = useRef8(true);
|
|
4903
|
+
useEffect15(() => {
|
|
4904
|
+
mountedRef.current = true;
|
|
4905
|
+
return () => {
|
|
4906
|
+
mountedRef.current = false;
|
|
4907
|
+
void generatorRef.current?.return(void 0);
|
|
4908
|
+
};
|
|
4909
|
+
}, []);
|
|
4910
|
+
useEffect15(() => {
|
|
4911
|
+
void fetchSnapshots(isPro());
|
|
4912
|
+
}, []);
|
|
4913
|
+
const runRollback = useCallback3(async (p) => {
|
|
4914
|
+
setPhase("executing");
|
|
4915
|
+
setStreamLines([]);
|
|
4916
|
+
setStreamError(null);
|
|
4917
|
+
setStreamRunning(true);
|
|
4918
|
+
const gen = executeRollbackPlan(p, isPro());
|
|
4919
|
+
generatorRef.current = gen;
|
|
4920
|
+
try {
|
|
4921
|
+
for await (const line of gen) {
|
|
4922
|
+
if (!mountedRef.current) break;
|
|
4923
|
+
setStreamLines((prev) => [...prev.slice(-99), line]);
|
|
4924
|
+
}
|
|
4925
|
+
} catch (err) {
|
|
4926
|
+
if (mountedRef.current) {
|
|
4927
|
+
setStreamError(err instanceof Error ? err.message : String(err));
|
|
4928
|
+
}
|
|
4929
|
+
} finally {
|
|
4930
|
+
generatorRef.current = null;
|
|
4931
|
+
if (mountedRef.current) {
|
|
4932
|
+
setStreamRunning(false);
|
|
4933
|
+
setPhase("result");
|
|
4934
|
+
}
|
|
4935
|
+
}
|
|
4936
|
+
}, [isPro]);
|
|
4937
|
+
useInput14((input, key) => {
|
|
4938
|
+
if (phase === "executing") return;
|
|
4939
|
+
if (phase === "result") {
|
|
4940
|
+
if (key.escape || input === "r") {
|
|
4941
|
+
setPhase("list");
|
|
4942
|
+
clearPlan();
|
|
4943
|
+
void fetchSnapshots(isPro());
|
|
4944
|
+
}
|
|
4945
|
+
return;
|
|
4946
|
+
}
|
|
4947
|
+
if (phase === "confirm") return;
|
|
4948
|
+
if (phase === "plan") {
|
|
4949
|
+
if (key.escape) {
|
|
4950
|
+
clearPlan();
|
|
4951
|
+
setPhase("list");
|
|
4952
|
+
return;
|
|
4953
|
+
}
|
|
4954
|
+
if (key.return && plan?.canExecute) {
|
|
4955
|
+
setPhase("confirm");
|
|
4956
|
+
}
|
|
4957
|
+
return;
|
|
4958
|
+
}
|
|
4959
|
+
if (input === "j" || key.downArrow) {
|
|
4960
|
+
setCursor((c) => Math.min(c + 1, Math.max(0, snapshots.length - 1)));
|
|
4961
|
+
} else if (input === "k" || key.upArrow) {
|
|
4962
|
+
setCursor((c) => Math.max(c - 1, 0));
|
|
4963
|
+
} else if (key.return && snapshots[cursor]) {
|
|
4964
|
+
void selectSnapshot(snapshots[cursor], isPro());
|
|
4965
|
+
setPhase("plan");
|
|
4966
|
+
} else if (input === "r") {
|
|
4967
|
+
void fetchSnapshots(isPro());
|
|
4968
|
+
}
|
|
4969
|
+
});
|
|
4970
|
+
if (loading) return /* @__PURE__ */ jsx31(Loading, { message: t("rollback_select_snapshot") });
|
|
4971
|
+
if (error) return /* @__PURE__ */ jsx31(ErrorMessage, { message: error });
|
|
4972
|
+
if (phase === "executing") {
|
|
4973
|
+
return /* @__PURE__ */ jsx31(Box29, { flexDirection: "column", children: /* @__PURE__ */ jsx31(ProgressLog, { lines: streamLines, isRunning: streamRunning, title: t("rollback_executing") }) });
|
|
4974
|
+
}
|
|
4975
|
+
if (phase === "result") {
|
|
4976
|
+
return /* @__PURE__ */ jsxs30(Box29, { flexDirection: "column", marginTop: 1, children: [
|
|
4977
|
+
/* @__PURE__ */ jsx31(
|
|
4978
|
+
ResultBanner,
|
|
4979
|
+
{
|
|
4980
|
+
status: streamError ? "error" : "success",
|
|
4981
|
+
message: streamError ? t("rollback_error", { error: streamError }) : t("rollback_success")
|
|
4982
|
+
}
|
|
4983
|
+
),
|
|
4984
|
+
/* @__PURE__ */ jsx31(Box29, { marginTop: 1, children: /* @__PURE__ */ jsxs30(Text30, { color: COLORS.textSecondary, children: [
|
|
4985
|
+
"r:",
|
|
4986
|
+
t("hint_refresh"),
|
|
4987
|
+
" esc:",
|
|
4988
|
+
t("hint_back")
|
|
4989
|
+
] }) })
|
|
4990
|
+
] });
|
|
4991
|
+
}
|
|
4992
|
+
return /* @__PURE__ */ jsxs30(Box29, { flexDirection: "column", children: [
|
|
4993
|
+
/* @__PURE__ */ jsx31(SectionHeader, { emoji: "\u23EA", title: t("rollback_title"), gradient: GRADIENTS.gold }),
|
|
4994
|
+
snapshots.length === 0 && /* @__PURE__ */ jsx31(Box29, { marginTop: 1, children: /* @__PURE__ */ jsx31(ResultBanner, { status: "info", message: t("rollback_no_snapshots") }) }),
|
|
4995
|
+
phase === "list" && snapshots.length > 0 && /* @__PURE__ */ jsxs30(Box29, { flexDirection: "column", marginTop: 1, children: [
|
|
4996
|
+
/* @__PURE__ */ jsx31(Text30, { color: COLORS.textSecondary, dimColor: true, children: t("rollback_select_snapshot") }),
|
|
4997
|
+
/* @__PURE__ */ jsx31(Box29, { flexDirection: "column", marginTop: 1, children: snapshots.map((s, i) => /* @__PURE__ */ jsxs30(SelectableRow, { isCurrent: i === cursor, children: [
|
|
4998
|
+
/* @__PURE__ */ jsx31(Text30, { bold: i === cursor, color: i === cursor ? COLORS.text : COLORS.muted, children: s.label ?? t("rollback_snapshot_auto") }),
|
|
4999
|
+
/* @__PURE__ */ jsxs30(Text30, { color: COLORS.textSecondary, children: [
|
|
5000
|
+
" \u2014 ",
|
|
5001
|
+
new Date(s.capturedAt).toLocaleString()
|
|
5002
|
+
] }),
|
|
5003
|
+
/* @__PURE__ */ jsxs30(Text30, { color: COLORS.muted, dimColor: true, children: [
|
|
5004
|
+
" ",
|
|
5005
|
+
"(",
|
|
5006
|
+
tp("packages", s.formulae.length + s.casks.length),
|
|
5007
|
+
")"
|
|
5008
|
+
] })
|
|
5009
|
+
] }, s.capturedAt)) })
|
|
5010
|
+
] }),
|
|
5011
|
+
phase === "plan" && /* @__PURE__ */ jsxs30(Box29, { flexDirection: "column", children: [
|
|
5012
|
+
planLoading && /* @__PURE__ */ jsx31(Loading, { message: t("rollback_capturing") }),
|
|
5013
|
+
planError && /* @__PURE__ */ jsx31(ErrorMessage, { message: planError }),
|
|
5014
|
+
plan && !planLoading && /* @__PURE__ */ jsx31(PlanView, { plan })
|
|
5015
|
+
] }),
|
|
5016
|
+
phase === "confirm" && plan && /* @__PURE__ */ jsx31(Box29, { marginTop: 1, children: /* @__PURE__ */ jsx31(
|
|
5017
|
+
ConfirmDialog,
|
|
5018
|
+
{
|
|
5019
|
+
message: t("rollback_confirm", {
|
|
5020
|
+
count: String(plan.actions.filter((a) => a.strategy !== "unavailable" && a.action !== "remove").length)
|
|
5021
|
+
}),
|
|
5022
|
+
onConfirm: () => void runRollback(plan),
|
|
5023
|
+
onCancel: () => setPhase("plan")
|
|
5024
|
+
}
|
|
5025
|
+
) })
|
|
5026
|
+
] });
|
|
5027
|
+
}
|
|
5028
|
+
|
|
5029
|
+
// src/views/brewfile.tsx
|
|
5030
|
+
import { useCallback as useCallback4, useEffect as useEffect16, useRef as useRef9, useState as useState14 } from "react";
|
|
5031
|
+
import { Box as Box30, Text as Text31, useInput as useInput15 } from "ink";
|
|
5032
|
+
import { TextInput as TextInput6 } from "@inkjs/ui";
|
|
5033
|
+
import { jsx as jsx32, jsxs as jsxs31 } from "react/jsx-runtime";
|
|
5034
|
+
function DriftScore({ score }) {
|
|
5035
|
+
const color = score >= 80 ? COLORS.success : score >= 50 ? COLORS.warning : COLORS.error;
|
|
5036
|
+
const bars = Math.round(score / 10);
|
|
5037
|
+
const filled = "\u2593".repeat(bars);
|
|
5038
|
+
const empty = "\u2591".repeat(10 - bars);
|
|
5039
|
+
return /* @__PURE__ */ jsxs31(Box30, { children: [
|
|
5040
|
+
/* @__PURE__ */ jsxs31(Text31, { color, children: [
|
|
5041
|
+
filled,
|
|
5042
|
+
empty
|
|
5043
|
+
] }),
|
|
5044
|
+
/* @__PURE__ */ jsxs31(Text31, { color, bold: true, children: [
|
|
5045
|
+
" ",
|
|
5046
|
+
score,
|
|
5047
|
+
"% "
|
|
5048
|
+
] }),
|
|
5049
|
+
/* @__PURE__ */ jsx32(Text31, { color: COLORS.textSecondary, children: t("brewfile_compliant") })
|
|
5050
|
+
] });
|
|
5051
|
+
}
|
|
5052
|
+
function DriftSummary({ drift }) {
|
|
5053
|
+
return /* @__PURE__ */ jsxs31(Box30, { flexDirection: "column", marginTop: 1, children: [
|
|
5054
|
+
drift.missingPackages.length > 0 && /* @__PURE__ */ jsxs31(Box30, { children: [
|
|
5055
|
+
/* @__PURE__ */ jsx32(Text31, { color: COLORS.error, children: "\u25CF " }),
|
|
5056
|
+
/* @__PURE__ */ jsx32(Text31, { color: COLORS.error, children: t("brewfile_drift_missing", { count: drift.missingPackages.length }) }),
|
|
5057
|
+
/* @__PURE__ */ jsxs31(Text31, { color: COLORS.textSecondary, children: [
|
|
5058
|
+
": " + drift.missingPackages.slice(0, 3).join(", "),
|
|
5059
|
+
drift.missingPackages.length > 3 ? "..." : ""
|
|
5060
|
+
] })
|
|
5061
|
+
] }),
|
|
5062
|
+
drift.extraPackages.length > 0 && /* @__PURE__ */ jsxs31(Box30, { children: [
|
|
5063
|
+
/* @__PURE__ */ jsx32(Text31, { color: COLORS.warning, children: "\u25CF " }),
|
|
5064
|
+
/* @__PURE__ */ jsx32(Text31, { color: COLORS.warning, children: t("brewfile_drift_extra", { count: drift.extraPackages.length }) })
|
|
5065
|
+
] }),
|
|
5066
|
+
drift.wrongVersions.length > 0 && /* @__PURE__ */ jsxs31(Box30, { children: [
|
|
5067
|
+
/* @__PURE__ */ jsx32(Text31, { color: COLORS.info, children: "\u25CF " }),
|
|
5068
|
+
/* @__PURE__ */ jsx32(Text31, { color: COLORS.info, children: t("brewfile_drift_wrong", { count: drift.wrongVersions.length }) })
|
|
5069
|
+
] }),
|
|
5070
|
+
drift.missingPackages.length === 0 && drift.extraPackages.length === 0 && drift.wrongVersions.length === 0 && /* @__PURE__ */ jsx32(ResultBanner, { status: "success", message: "\u2713 System is in sync with Brewfile" })
|
|
5071
|
+
] });
|
|
5072
|
+
}
|
|
5073
|
+
function BrewfileView() {
|
|
5074
|
+
const isPro = useLicenseStore((s) => s.isPro);
|
|
5075
|
+
const { schema, drift, loading, driftLoading, error, load, createFromCurrent } = useBrewfileStore();
|
|
5076
|
+
const [phase, setPhase] = useState14("overview");
|
|
5077
|
+
const [streamLines, setStreamLines] = useState14([]);
|
|
5078
|
+
const [streamRunning, setStreamRunning] = useState14(false);
|
|
5079
|
+
const [streamError, setStreamError] = useState14(null);
|
|
5080
|
+
const [resultMessage, setResultMessage] = useState14("");
|
|
5081
|
+
const generatorRef = useRef9(null);
|
|
5082
|
+
const mountedRef = useRef9(true);
|
|
5083
|
+
useEffect16(() => {
|
|
5084
|
+
mountedRef.current = true;
|
|
5085
|
+
void load();
|
|
5086
|
+
return () => {
|
|
5087
|
+
mountedRef.current = false;
|
|
5088
|
+
void generatorRef.current?.return(void 0);
|
|
5089
|
+
};
|
|
5090
|
+
}, []);
|
|
5091
|
+
const startReconcile = useCallback4(async () => {
|
|
5092
|
+
if (!schema) return;
|
|
5093
|
+
setPhase("reconciling");
|
|
5094
|
+
setStreamLines([]);
|
|
5095
|
+
setStreamError(null);
|
|
5096
|
+
setStreamRunning(true);
|
|
5097
|
+
const gen = reconcile(schema, isPro());
|
|
5098
|
+
generatorRef.current = gen;
|
|
5099
|
+
try {
|
|
5100
|
+
for await (const line of gen) {
|
|
5101
|
+
if (!mountedRef.current) break;
|
|
5102
|
+
setStreamLines((prev) => [...prev.slice(-99), line]);
|
|
5103
|
+
}
|
|
5104
|
+
if (mountedRef.current) {
|
|
5105
|
+
setResultMessage(t("brewfile_reconcile_success"));
|
|
5106
|
+
}
|
|
5107
|
+
} catch (err) {
|
|
5108
|
+
if (mountedRef.current) {
|
|
5109
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5110
|
+
setStreamError(msg);
|
|
5111
|
+
setResultMessage(t("brewfile_reconcile_error", { error: msg }));
|
|
5112
|
+
}
|
|
5113
|
+
} finally {
|
|
5114
|
+
generatorRef.current = null;
|
|
5115
|
+
if (mountedRef.current) {
|
|
5116
|
+
setStreamRunning(false);
|
|
5117
|
+
setPhase("result");
|
|
5118
|
+
}
|
|
5119
|
+
}
|
|
5120
|
+
}, [schema, isPro]);
|
|
5121
|
+
useInput15((input, key) => {
|
|
5122
|
+
if (phase === "reconciling") return;
|
|
5123
|
+
if (phase === "result") {
|
|
5124
|
+
if (key.escape || input === "r") {
|
|
5125
|
+
setPhase("overview");
|
|
5126
|
+
void load();
|
|
5127
|
+
}
|
|
5128
|
+
return;
|
|
5129
|
+
}
|
|
5130
|
+
if (phase === "creating") return;
|
|
5131
|
+
if (input === "n") {
|
|
5132
|
+
setPhase("creating");
|
|
5133
|
+
return;
|
|
5134
|
+
}
|
|
5135
|
+
if (input === "r") {
|
|
5136
|
+
void load();
|
|
5137
|
+
return;
|
|
5138
|
+
}
|
|
5139
|
+
if (input === "c") {
|
|
5140
|
+
const needsReconcile = drift && (drift.missingPackages.length > 0 || drift.wrongVersions.length > 0);
|
|
5141
|
+
if (needsReconcile) {
|
|
5142
|
+
void startReconcile();
|
|
5143
|
+
}
|
|
5144
|
+
return;
|
|
5145
|
+
}
|
|
5146
|
+
if (key.escape) {
|
|
5147
|
+
}
|
|
5148
|
+
});
|
|
5149
|
+
if (loading) return /* @__PURE__ */ jsx32(Loading, { message: t("loading_default") });
|
|
5150
|
+
if (error) return /* @__PURE__ */ jsx32(ErrorMessage, { message: error });
|
|
5151
|
+
if (phase === "creating") {
|
|
5152
|
+
return /* @__PURE__ */ jsxs31(Box30, { flexDirection: "column", marginTop: 1, children: [
|
|
5153
|
+
/* @__PURE__ */ jsx32(SectionHeader, { emoji: "\u{1F4E6}", title: t("brewfile_title"), gradient: GRADIENTS.ocean }),
|
|
5154
|
+
/* @__PURE__ */ jsxs31(Box30, { marginTop: 1, children: [
|
|
5155
|
+
/* @__PURE__ */ jsxs31(Text31, { color: COLORS.textSecondary, children: [
|
|
5156
|
+
t("brewfile_create_name"),
|
|
5157
|
+
" "
|
|
5158
|
+
] }),
|
|
5159
|
+
/* @__PURE__ */ jsx32(
|
|
5160
|
+
TextInput6,
|
|
5161
|
+
{
|
|
5162
|
+
defaultValue: "My Environment",
|
|
5163
|
+
onSubmit: (value) => {
|
|
5164
|
+
const name = value.trim() || "My Environment";
|
|
5165
|
+
setPhase("overview");
|
|
5166
|
+
void createFromCurrent(name).then(() => {
|
|
5167
|
+
});
|
|
5168
|
+
}
|
|
5169
|
+
}
|
|
5170
|
+
)
|
|
5171
|
+
] })
|
|
5172
|
+
] });
|
|
5173
|
+
}
|
|
5174
|
+
if (phase === "reconciling") {
|
|
5175
|
+
return /* @__PURE__ */ jsx32(Box30, { flexDirection: "column", children: /* @__PURE__ */ jsx32(
|
|
5176
|
+
ProgressLog,
|
|
5177
|
+
{
|
|
5178
|
+
lines: streamLines,
|
|
5179
|
+
isRunning: streamRunning,
|
|
5180
|
+
title: t("brewfile_reconciling")
|
|
5181
|
+
}
|
|
5182
|
+
) });
|
|
5183
|
+
}
|
|
5184
|
+
if (phase === "result") {
|
|
5185
|
+
return /* @__PURE__ */ jsxs31(Box30, { flexDirection: "column", marginTop: 1, children: [
|
|
5186
|
+
/* @__PURE__ */ jsx32(
|
|
5187
|
+
ResultBanner,
|
|
5188
|
+
{
|
|
5189
|
+
status: streamError ? "error" : "success",
|
|
5190
|
+
message: resultMessage
|
|
5191
|
+
}
|
|
5192
|
+
),
|
|
5193
|
+
/* @__PURE__ */ jsx32(Box30, { marginTop: 1, children: /* @__PURE__ */ jsxs31(Text31, { color: COLORS.textSecondary, children: [
|
|
5194
|
+
"r:",
|
|
5195
|
+
t("hint_refresh"),
|
|
5196
|
+
" esc:",
|
|
5197
|
+
t("hint_back")
|
|
5198
|
+
] }) })
|
|
5199
|
+
] });
|
|
5200
|
+
}
|
|
5201
|
+
return /* @__PURE__ */ jsxs31(Box30, { flexDirection: "column", children: [
|
|
5202
|
+
/* @__PURE__ */ jsx32(SectionHeader, { emoji: "\u{1F4E6}", title: t("brewfile_title"), gradient: GRADIENTS.ocean }),
|
|
5203
|
+
schema === null ? /* @__PURE__ */ jsxs31(Box30, { marginTop: 1, flexDirection: "column", children: [
|
|
5204
|
+
/* @__PURE__ */ jsx32(ResultBanner, { status: "info", message: t("brewfile_no_brewfile") }),
|
|
5205
|
+
/* @__PURE__ */ jsx32(Box30, { marginTop: 1, children: /* @__PURE__ */ jsxs31(Text31, { color: COLORS.textSecondary, children: [
|
|
5206
|
+
"n:",
|
|
5207
|
+
t("hint_new")
|
|
5208
|
+
] }) })
|
|
5209
|
+
] }) : /* @__PURE__ */ jsxs31(Box30, { flexDirection: "column", marginTop: 1, children: [
|
|
5210
|
+
/* @__PURE__ */ jsxs31(Box30, { gap: 2, children: [
|
|
5211
|
+
/* @__PURE__ */ jsx32(Text31, { color: COLORS.text, bold: true, children: schema.meta.name }),
|
|
5212
|
+
schema.meta.description && /* @__PURE__ */ jsx32(Text31, { color: COLORS.textSecondary, children: schema.meta.description }),
|
|
5213
|
+
schema.strictMode && /* @__PURE__ */ jsxs31(Text31, { color: COLORS.warning, children: [
|
|
5214
|
+
"[",
|
|
5215
|
+
t("brewfile_strict_mode"),
|
|
5216
|
+
"]"
|
|
5217
|
+
] })
|
|
5218
|
+
] }),
|
|
5219
|
+
/* @__PURE__ */ jsxs31(Box30, { gap: 3, marginTop: 1, children: [
|
|
5220
|
+
/* @__PURE__ */ jsx32(Text31, { color: COLORS.sky, children: t("brewfile_formulae_count", { count: schema.formulae.length }) }),
|
|
5221
|
+
/* @__PURE__ */ jsx32(Text31, { color: COLORS.teal, children: t("brewfile_casks_count", { count: schema.casks.length }) })
|
|
5222
|
+
] }),
|
|
5223
|
+
driftLoading && /* @__PURE__ */ jsx32(Box30, { marginTop: 1, children: /* @__PURE__ */ jsx32(Text31, { color: COLORS.muted, children: "Computing drift..." }) }),
|
|
5224
|
+
drift && !driftLoading && /* @__PURE__ */ jsxs31(Box30, { flexDirection: "column", marginTop: 1, children: [
|
|
5225
|
+
/* @__PURE__ */ jsx32(DriftScore, { score: drift.score }),
|
|
5226
|
+
/* @__PURE__ */ jsx32(DriftSummary, { drift })
|
|
5227
|
+
] }),
|
|
5228
|
+
/* @__PURE__ */ jsx32(Box30, { marginTop: 1, children: /* @__PURE__ */ jsxs31(Text31, { color: COLORS.textSecondary, children: [
|
|
5229
|
+
"r:",
|
|
5230
|
+
t("hint_refresh"),
|
|
5231
|
+
drift && (drift.missingPackages.length > 0 || drift.wrongVersions.length > 0) ? ` c:${t("hint_reconcile")}` : "",
|
|
5232
|
+
" ",
|
|
5233
|
+
"n:",
|
|
5234
|
+
t("hint_new")
|
|
5235
|
+
] }) })
|
|
5236
|
+
] })
|
|
5237
|
+
] });
|
|
5238
|
+
}
|
|
5239
|
+
|
|
5240
|
+
// src/views/sync.tsx
|
|
5241
|
+
import { useCallback as useCallback5, useEffect as useEffect17, useState as useState15 } from "react";
|
|
5242
|
+
import { Box as Box31, Text as Text32, useInput as useInput16 } from "ink";
|
|
5243
|
+
import { Fragment as Fragment6, jsx as jsx33, jsxs as jsxs32 } from "react/jsx-runtime";
|
|
5244
|
+
function OverviewSection({
|
|
5245
|
+
config,
|
|
5246
|
+
lastResult,
|
|
5247
|
+
conflicts,
|
|
5248
|
+
onSyncNow: _onSyncNow,
|
|
5249
|
+
onGoToConflicts: _onGoToConflicts
|
|
5250
|
+
}) {
|
|
5251
|
+
const hasConflicts = conflicts.length > 0;
|
|
5252
|
+
const showComplianceHint = !hasConflicts && !!lastResult?.success;
|
|
5253
|
+
return /* @__PURE__ */ jsxs32(Box31, { flexDirection: "column", marginTop: 1, children: [
|
|
5254
|
+
config ? /* @__PURE__ */ jsxs32(Fragment6, { children: [
|
|
5255
|
+
/* @__PURE__ */ jsx33(Box31, { marginBottom: 1, children: /* @__PURE__ */ jsx33(Text32, { color: COLORS.textSecondary, children: t("sync_machine", { name: config.machineName }) }) }),
|
|
5256
|
+
config.lastSync && /* @__PURE__ */ jsx33(Box31, { marginBottom: 1, children: /* @__PURE__ */ jsx33(Text32, { color: COLORS.textSecondary, children: t("sync_last_sync", { date: new Date(config.lastSync).toLocaleString() }) }) }),
|
|
5257
|
+
hasConflicts ? /* @__PURE__ */ jsx33(
|
|
5258
|
+
ResultBanner,
|
|
5259
|
+
{
|
|
5260
|
+
status: "error",
|
|
5261
|
+
message: t("sync_status_conflict", { count: String(conflicts.length) })
|
|
5262
|
+
}
|
|
5263
|
+
) : lastResult?.success ? /* @__PURE__ */ jsx33(ResultBanner, { status: "success", message: t("sync_status_ok") }) : null
|
|
5264
|
+
] }) : /* @__PURE__ */ jsx33(Box31, { marginBottom: 1, children: /* @__PURE__ */ jsx33(Text32, { color: COLORS.textSecondary, children: t("sync_disabled") }) }),
|
|
5265
|
+
/* @__PURE__ */ jsx33(Box31, { marginTop: 1, children: /* @__PURE__ */ jsxs32(Text32, { color: COLORS.textSecondary, children: [
|
|
5266
|
+
"s",
|
|
5267
|
+
/* @__PURE__ */ jsxs32(Text32, { color: COLORS.gold, children: [
|
|
5268
|
+
":",
|
|
5269
|
+
t("hint_sync")
|
|
5270
|
+
] }),
|
|
5271
|
+
hasConflicts && /* @__PURE__ */ jsxs32(Fragment6, { children: [
|
|
5272
|
+
" c",
|
|
5273
|
+
/* @__PURE__ */ jsxs32(Text32, { color: COLORS.gold, children: [
|
|
5274
|
+
":",
|
|
5275
|
+
t("hint_conflict")
|
|
5276
|
+
] })
|
|
5277
|
+
] }),
|
|
5278
|
+
showComplianceHint && /* @__PURE__ */ jsxs32(Fragment6, { children: [
|
|
5279
|
+
" c",
|
|
5280
|
+
/* @__PURE__ */ jsxs32(Text32, { color: COLORS.gold, children: [
|
|
5281
|
+
":",
|
|
5282
|
+
t("hint_check_compliance")
|
|
5283
|
+
] })
|
|
5284
|
+
] })
|
|
5285
|
+
] }) })
|
|
5286
|
+
] });
|
|
5287
|
+
}
|
|
5288
|
+
function ConflictsList({
|
|
5289
|
+
entries,
|
|
5290
|
+
cursor
|
|
5291
|
+
}) {
|
|
5292
|
+
return /* @__PURE__ */ jsxs32(Box31, { flexDirection: "column", marginTop: 1, children: [
|
|
5293
|
+
entries.map((entry, i) => {
|
|
5294
|
+
const { conflict, resolution } = entry;
|
|
5295
|
+
const isActive = i === cursor;
|
|
5296
|
+
return /* @__PURE__ */ jsxs32(Box31, { flexDirection: "column", marginBottom: 1, children: [
|
|
5297
|
+
/* @__PURE__ */ jsxs32(SelectableRow, { isCurrent: isActive, children: [
|
|
5298
|
+
/* @__PURE__ */ jsx33(Text32, { bold: true, color: isActive ? COLORS.text : COLORS.textSecondary, children: t("sync_conflict_title", { package: conflict.packageName }) }),
|
|
5299
|
+
/* @__PURE__ */ jsxs32(Text32, { color: COLORS.muted, children: [
|
|
5300
|
+
" (",
|
|
5301
|
+
conflict.packageType,
|
|
5302
|
+
")"
|
|
5303
|
+
] })
|
|
5304
|
+
] }),
|
|
5305
|
+
/* @__PURE__ */ jsxs32(Box31, { marginLeft: 2, flexDirection: "column", children: [
|
|
5306
|
+
/* @__PURE__ */ jsxs32(
|
|
5307
|
+
Text32,
|
|
5308
|
+
{
|
|
5309
|
+
color: resolution === "use-local" ? COLORS.success : COLORS.textSecondary,
|
|
5310
|
+
children: [
|
|
5311
|
+
"l ",
|
|
5312
|
+
t("sync_conflict_local", { version: conflict.localVersion }),
|
|
5313
|
+
resolution === "use-local" && " \u2713"
|
|
5314
|
+
]
|
|
5315
|
+
}
|
|
5316
|
+
),
|
|
5317
|
+
/* @__PURE__ */ jsxs32(
|
|
5318
|
+
Text32,
|
|
5319
|
+
{
|
|
5320
|
+
color: resolution === "use-remote" ? COLORS.success : COLORS.textSecondary,
|
|
5321
|
+
children: [
|
|
5322
|
+
"r ",
|
|
5323
|
+
t("sync_conflict_remote", { machine: conflict.remoteMachine, version: conflict.remoteVersion }),
|
|
5324
|
+
resolution === "use-remote" && " \u2713"
|
|
5325
|
+
]
|
|
5326
|
+
}
|
|
5327
|
+
)
|
|
5328
|
+
] })
|
|
5329
|
+
] }, `${conflict.packageName}-${conflict.remoteMachine}`);
|
|
5330
|
+
}),
|
|
5331
|
+
/* @__PURE__ */ jsx33(Box31, { marginTop: 1, children: /* @__PURE__ */ jsxs32(Text32, { color: COLORS.textSecondary, children: [
|
|
5332
|
+
"j/k:navegar l:",
|
|
5333
|
+
t("sync_conflict_use_local"),
|
|
5334
|
+
" r:",
|
|
5335
|
+
t("sync_conflict_use_remote"),
|
|
5336
|
+
" enter:aplicar"
|
|
5337
|
+
] }) })
|
|
5338
|
+
] });
|
|
5339
|
+
}
|
|
5340
|
+
function SyncView() {
|
|
5341
|
+
const isPro = useLicenseStore((s) => s.isPro);
|
|
5342
|
+
const navigate = useNavigationStore((s) => s.navigate);
|
|
5343
|
+
const { config, lastResult, conflicts, loading, error, initialize, syncNow, resolveConflicts } = useSyncStore();
|
|
5344
|
+
const [phase, setPhase] = useState15("overview");
|
|
5345
|
+
const [syncError, setSyncError] = useState15(null);
|
|
5346
|
+
const [conflictEntries, setConflictEntries] = useState15([]);
|
|
5347
|
+
const [cursor, setCursor] = useState15(0);
|
|
5348
|
+
useEffect17(() => {
|
|
5349
|
+
void initialize(isPro());
|
|
5350
|
+
}, []);
|
|
5351
|
+
useEffect17(() => {
|
|
5352
|
+
if (conflicts.length > 0) {
|
|
5353
|
+
setConflictEntries(
|
|
5354
|
+
conflicts.map((c) => ({ conflict: c, resolution: "pending" }))
|
|
5355
|
+
);
|
|
5356
|
+
}
|
|
5357
|
+
}, [conflicts]);
|
|
5358
|
+
const handleSyncNow = useCallback5(async () => {
|
|
5359
|
+
setPhase("syncing");
|
|
5360
|
+
setSyncError(null);
|
|
5361
|
+
await syncNow(isPro());
|
|
5362
|
+
const state = useSyncStore.getState();
|
|
5363
|
+
if (state.conflicts.length > 0) {
|
|
5364
|
+
setPhase("conflicts");
|
|
5365
|
+
} else if (state.error) {
|
|
5366
|
+
setSyncError(state.error);
|
|
5367
|
+
setPhase("result");
|
|
5368
|
+
} else {
|
|
5369
|
+
setPhase("result");
|
|
5370
|
+
}
|
|
5371
|
+
}, [isPro, syncNow]);
|
|
5372
|
+
const handleApplyResolutions = useCallback5(async () => {
|
|
5373
|
+
const pending = conflictEntries.filter((e) => e.resolution === "pending");
|
|
5374
|
+
if (pending.length > 0) return;
|
|
5375
|
+
const resolutions = conflictEntries.map((e) => ({
|
|
5376
|
+
conflict: e.conflict,
|
|
5377
|
+
resolution: e.resolution
|
|
5378
|
+
}));
|
|
5379
|
+
await resolveConflicts(resolutions);
|
|
5380
|
+
setPhase("result");
|
|
5381
|
+
}, [conflictEntries, resolveConflicts]);
|
|
5382
|
+
useInput16((input, key) => {
|
|
5383
|
+
if (phase === "syncing") return;
|
|
5384
|
+
if (phase === "result") {
|
|
5385
|
+
if (key.escape || input === "r") {
|
|
5386
|
+
setPhase("overview");
|
|
5387
|
+
setSyncError(null);
|
|
5388
|
+
void initialize(isPro());
|
|
5389
|
+
}
|
|
5390
|
+
return;
|
|
5391
|
+
}
|
|
5392
|
+
if (phase === "conflicts") {
|
|
5393
|
+
if (key.escape) {
|
|
5394
|
+
setPhase("overview");
|
|
5395
|
+
return;
|
|
5396
|
+
}
|
|
5397
|
+
if (input === "j" || key.downArrow) {
|
|
5398
|
+
setCursor((c) => Math.min(c + 1, conflictEntries.length - 1));
|
|
5399
|
+
return;
|
|
5400
|
+
}
|
|
5401
|
+
if (input === "k" || key.upArrow) {
|
|
5402
|
+
setCursor((c) => Math.max(c - 1, 0));
|
|
5403
|
+
return;
|
|
5404
|
+
}
|
|
5405
|
+
if (input === "l") {
|
|
5406
|
+
setConflictEntries(
|
|
5407
|
+
(prev) => prev.map((e, i) => i === cursor ? { ...e, resolution: "use-local" } : e)
|
|
5408
|
+
);
|
|
5409
|
+
return;
|
|
5410
|
+
}
|
|
5411
|
+
if (input === "r") {
|
|
5412
|
+
setConflictEntries(
|
|
5413
|
+
(prev) => prev.map((e, i) => i === cursor ? { ...e, resolution: "use-remote" } : e)
|
|
5414
|
+
);
|
|
5415
|
+
return;
|
|
5416
|
+
}
|
|
5417
|
+
if (key.return) {
|
|
5418
|
+
void handleApplyResolutions();
|
|
5419
|
+
return;
|
|
5420
|
+
}
|
|
5421
|
+
}
|
|
5422
|
+
if (input === "s") {
|
|
5423
|
+
void handleSyncNow();
|
|
5424
|
+
return;
|
|
5425
|
+
}
|
|
5426
|
+
if (input === "c" && conflicts.length > 0) {
|
|
5427
|
+
setCursor(0);
|
|
5428
|
+
setPhase("conflicts");
|
|
5429
|
+
return;
|
|
5430
|
+
}
|
|
5431
|
+
if (input === "c" && lastResult?.success) {
|
|
5432
|
+
navigate("compliance");
|
|
5433
|
+
return;
|
|
5434
|
+
}
|
|
5435
|
+
if (input === "r") {
|
|
5436
|
+
void initialize(isPro());
|
|
5437
|
+
return;
|
|
5438
|
+
}
|
|
5439
|
+
});
|
|
5440
|
+
if (phase === "syncing" || loading) {
|
|
5441
|
+
return /* @__PURE__ */ jsx33(Loading, { message: t("sync_syncing") });
|
|
5442
|
+
}
|
|
5443
|
+
if (phase === "result") {
|
|
5444
|
+
const isError = !!(syncError ?? error);
|
|
5445
|
+
return /* @__PURE__ */ jsxs32(Box31, { flexDirection: "column", marginTop: 1, children: [
|
|
5446
|
+
/* @__PURE__ */ jsx33(SectionHeader, { emoji: "\u{1F504}", title: t("sync_title"), gradient: GRADIENTS.gold }),
|
|
5447
|
+
/* @__PURE__ */ jsx33(Box31, { marginTop: 1, children: /* @__PURE__ */ jsx33(
|
|
5448
|
+
ResultBanner,
|
|
5449
|
+
{
|
|
5450
|
+
status: isError ? "error" : "success",
|
|
5451
|
+
message: isError ? t("sync_error", { error: syncError ?? error ?? "" }) : t("sync_success")
|
|
5452
|
+
}
|
|
5453
|
+
) }),
|
|
5454
|
+
/* @__PURE__ */ jsx33(Box31, { marginTop: 1, children: /* @__PURE__ */ jsxs32(Text32, { color: COLORS.textSecondary, children: [
|
|
5455
|
+
"r:",
|
|
5456
|
+
t("hint_refresh"),
|
|
5457
|
+
" esc:",
|
|
5458
|
+
t("hint_back")
|
|
5459
|
+
] }) })
|
|
5460
|
+
] });
|
|
5461
|
+
}
|
|
5462
|
+
return /* @__PURE__ */ jsxs32(Box31, { flexDirection: "column", children: [
|
|
5463
|
+
/* @__PURE__ */ jsx33(SectionHeader, { emoji: "\u{1F504}", title: t("sync_title"), gradient: GRADIENTS.gold }),
|
|
5464
|
+
error && phase === "overview" && /* @__PURE__ */ jsx33(Box31, { marginTop: 1, children: /* @__PURE__ */ jsx33(ResultBanner, { status: "error", message: t("sync_error", { error }) }) }),
|
|
5465
|
+
phase === "overview" && /* @__PURE__ */ jsx33(
|
|
5466
|
+
OverviewSection,
|
|
5467
|
+
{
|
|
5468
|
+
config,
|
|
5469
|
+
lastResult,
|
|
5470
|
+
conflicts,
|
|
5471
|
+
onSyncNow: () => void handleSyncNow(),
|
|
5472
|
+
onGoToConflicts: () => {
|
|
5473
|
+
setCursor(0);
|
|
5474
|
+
setPhase("conflicts");
|
|
5475
|
+
}
|
|
5476
|
+
}
|
|
5477
|
+
),
|
|
5478
|
+
phase === "conflicts" && /* @__PURE__ */ jsxs32(Box31, { flexDirection: "column", children: [
|
|
5479
|
+
/* @__PURE__ */ jsx33(SectionHeader, { emoji: "\u26A0", title: t("sync_status_conflict", { count: String(conflictEntries.length) }), gradient: GRADIENTS.gold }),
|
|
5480
|
+
/* @__PURE__ */ jsx33(ConflictsList, { entries: conflictEntries, cursor })
|
|
5481
|
+
] })
|
|
5482
|
+
] });
|
|
5483
|
+
}
|
|
5484
|
+
|
|
5485
|
+
// src/views/compliance.tsx
|
|
5486
|
+
import { useCallback as useCallback6, useEffect as useEffect18, useRef as useRef10, useState as useState16 } from "react";
|
|
5487
|
+
import { Box as Box32, Text as Text33, useInput as useInput17 } from "ink";
|
|
5488
|
+
import { TextInput as TextInput7 } from "@inkjs/ui";
|
|
5489
|
+
|
|
5490
|
+
// src/lib/compliance/compliance-remediator.ts
|
|
5491
|
+
async function* remediateViolations(violations, isPro) {
|
|
5492
|
+
if (!isPro) throw new Error("Pro license required");
|
|
5493
|
+
let installed = 0;
|
|
5494
|
+
let upgraded = 0;
|
|
5495
|
+
let skipped = 0;
|
|
5496
|
+
for (const v of violations) {
|
|
5497
|
+
if (v.type === "missing") {
|
|
5498
|
+
yield `Installing ${v.packageName}...`;
|
|
5499
|
+
try {
|
|
5500
|
+
for await (const line of streamBrew(["install", v.packageName])) {
|
|
5501
|
+
yield line;
|
|
5502
|
+
}
|
|
5503
|
+
installed++;
|
|
5504
|
+
} catch (err) {
|
|
5505
|
+
yield ` \u2717 Failed to install ${v.packageName}: ${err instanceof Error ? err.message : String(err)}`;
|
|
5506
|
+
skipped++;
|
|
5507
|
+
}
|
|
5508
|
+
} else if (v.type === "wrong-version") {
|
|
5509
|
+
yield `Upgrading ${v.packageName}${v.required ? ` to ${v.required}+` : ""}...`;
|
|
5510
|
+
try {
|
|
5511
|
+
for await (const line of streamBrew(["upgrade", v.packageName])) {
|
|
5512
|
+
yield line;
|
|
5513
|
+
}
|
|
5514
|
+
upgraded++;
|
|
5515
|
+
} catch (err) {
|
|
5516
|
+
yield ` \u2717 Failed to upgrade ${v.packageName}: ${err instanceof Error ? err.message : String(err)}`;
|
|
5517
|
+
skipped++;
|
|
5518
|
+
}
|
|
5519
|
+
} else if (v.type === "forbidden") {
|
|
5520
|
+
yield ` \u26A0 Forbidden package detected: ${v.packageName} \u2014 manual removal required`;
|
|
5521
|
+
skipped++;
|
|
5522
|
+
} else if (v.type === "extra") {
|
|
5523
|
+
yield ` \u26A0 Extra package in strict mode: ${v.packageName} \u2014 manual removal required`;
|
|
5524
|
+
skipped++;
|
|
5525
|
+
}
|
|
5526
|
+
}
|
|
5527
|
+
yield `Remediation complete: ${installed} installed, ${upgraded} upgraded, ${skipped} skipped (manual action required)`;
|
|
5528
|
+
}
|
|
5529
|
+
|
|
5530
|
+
// src/views/compliance.tsx
|
|
5531
|
+
import { join as join5 } from "path";
|
|
5532
|
+
import { jsx as jsx34, jsxs as jsxs33 } from "react/jsx-runtime";
|
|
5533
|
+
function ComplianceScore({ report }) {
|
|
5534
|
+
const color = report.score >= 80 ? COLORS.success : report.score >= 50 ? COLORS.warning : COLORS.error;
|
|
5535
|
+
const bars = Math.round(report.score / 10);
|
|
5536
|
+
return /* @__PURE__ */ jsxs33(Box32, { flexDirection: "column", marginBottom: 1, children: [
|
|
5537
|
+
/* @__PURE__ */ jsxs33(Box32, { children: [
|
|
5538
|
+
/* @__PURE__ */ jsxs33(Text33, { color, children: [
|
|
5539
|
+
"\u2593".repeat(bars),
|
|
5540
|
+
"\u2591".repeat(10 - bars)
|
|
5541
|
+
] }),
|
|
5542
|
+
/* @__PURE__ */ jsxs33(Text33, { color, bold: true, children: [
|
|
5543
|
+
" ",
|
|
5544
|
+
report.score,
|
|
5545
|
+
"%"
|
|
5546
|
+
] }),
|
|
5547
|
+
/* @__PURE__ */ jsxs33(Text33, { color: COLORS.textSecondary, children: [
|
|
5548
|
+
" ",
|
|
5549
|
+
t("compliance_score", { score: String(report.score) })
|
|
5550
|
+
] })
|
|
5551
|
+
] }),
|
|
5552
|
+
/* @__PURE__ */ jsxs33(Text33, { color: COLORS.muted, dimColor: true, children: [
|
|
5553
|
+
t("compliance_policy_name", { name: report.policyName }),
|
|
5554
|
+
" \xB7 ",
|
|
5555
|
+
t("compliance_machine", { name: report.machineName })
|
|
5556
|
+
] })
|
|
5557
|
+
] });
|
|
5558
|
+
}
|
|
5559
|
+
function ViolationItem({ violation }) {
|
|
5560
|
+
const color = violation.severity === "error" ? COLORS.error : COLORS.warning;
|
|
5561
|
+
const prefix = violation.severity === "error" ? "\u2717" : "\u26A0";
|
|
5562
|
+
return /* @__PURE__ */ jsxs33(Box32, { marginBottom: 0, children: [
|
|
5563
|
+
/* @__PURE__ */ jsxs33(Text33, { color, children: [
|
|
5564
|
+
prefix,
|
|
5565
|
+
" "
|
|
5566
|
+
] }),
|
|
5567
|
+
/* @__PURE__ */ jsx34(Text33, { color, children: violation.detail })
|
|
5568
|
+
] });
|
|
5569
|
+
}
|
|
5570
|
+
function ViolationList({ violations }) {
|
|
5571
|
+
const errors = violations.filter((v) => v.severity === "error");
|
|
5572
|
+
const warnings = violations.filter((v) => v.severity === "warning");
|
|
5573
|
+
return /* @__PURE__ */ jsxs33(Box32, { flexDirection: "column", marginTop: 1, children: [
|
|
5574
|
+
errors.length > 0 && /* @__PURE__ */ jsxs33(Box32, { flexDirection: "column", marginBottom: 1, children: [
|
|
5575
|
+
/* @__PURE__ */ jsxs33(Text33, { color: COLORS.error, bold: true, children: [
|
|
5576
|
+
t("compliance_violations", { count: String(errors.length) }),
|
|
5577
|
+
" (errors)"
|
|
5578
|
+
] }),
|
|
5579
|
+
errors.map((v) => /* @__PURE__ */ jsx34(ViolationItem, { violation: v }, `${v.type}-${v.packageName}`))
|
|
5580
|
+
] }),
|
|
5581
|
+
warnings.length > 0 && /* @__PURE__ */ jsxs33(Box32, { flexDirection: "column", children: [
|
|
5582
|
+
/* @__PURE__ */ jsxs33(Text33, { color: COLORS.warning, bold: true, children: [
|
|
5583
|
+
t("compliance_violations", { count: String(warnings.length) }),
|
|
5584
|
+
" (warnings)"
|
|
5585
|
+
] }),
|
|
5586
|
+
warnings.map((v) => /* @__PURE__ */ jsx34(ViolationItem, { violation: v }, `${v.type}-${v.packageName}`))
|
|
5587
|
+
] })
|
|
5588
|
+
] });
|
|
5589
|
+
}
|
|
5590
|
+
function ComplianceView() {
|
|
5591
|
+
const isPro = useLicenseStore((s) => s.isPro);
|
|
5592
|
+
const { policy, report, loading, error, importPolicy, runCheck } = useComplianceStore();
|
|
5593
|
+
const [phase, setPhase] = useState16("overview");
|
|
5594
|
+
const [resultMessage, setResultMessage] = useState16(null);
|
|
5595
|
+
const [streamLines, setStreamLines] = useState16([]);
|
|
5596
|
+
const [streamRunning, setStreamRunning] = useState16(false);
|
|
5597
|
+
const generatorRef = useRef10(null);
|
|
5598
|
+
const mountedRef = useRef10(true);
|
|
5599
|
+
useEffect18(() => {
|
|
5600
|
+
mountedRef.current = true;
|
|
5601
|
+
return () => {
|
|
5602
|
+
mountedRef.current = false;
|
|
5603
|
+
void generatorRef.current?.return(void 0);
|
|
5604
|
+
};
|
|
5605
|
+
}, []);
|
|
5606
|
+
const handleImportSubmit = useCallback6(
|
|
5607
|
+
async (filePath) => {
|
|
5608
|
+
if (!filePath.trim()) {
|
|
5609
|
+
setPhase("overview");
|
|
5610
|
+
return;
|
|
5611
|
+
}
|
|
5612
|
+
await importPolicy(filePath.trim(), isPro());
|
|
5613
|
+
const state = useComplianceStore.getState();
|
|
5614
|
+
if (!state.error && state.policy) {
|
|
5615
|
+
await runCheck(isPro());
|
|
5616
|
+
}
|
|
5617
|
+
setPhase("overview");
|
|
5618
|
+
},
|
|
5619
|
+
[isPro, importPolicy, runCheck]
|
|
5620
|
+
);
|
|
5621
|
+
const handleRecheck = useCallback6(() => {
|
|
5622
|
+
void runCheck(isPro());
|
|
5623
|
+
}, [isPro, runCheck]);
|
|
5624
|
+
const handleExport = useCallback6(async () => {
|
|
5625
|
+
if (!report) return;
|
|
5626
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
5627
|
+
const outputPath = join5(DATA_DIR, `compliance-report-${timestamp}.json`);
|
|
5628
|
+
try {
|
|
5629
|
+
await exportReport(report, outputPath);
|
|
5630
|
+
setResultMessage({ ok: true, text: t("compliance_export_done", { path: outputPath }) });
|
|
5631
|
+
setPhase("result");
|
|
5632
|
+
} catch (err) {
|
|
5633
|
+
setResultMessage({
|
|
5634
|
+
ok: false,
|
|
5635
|
+
text: t("compliance_remediate_error", { error: err instanceof Error ? err.message : String(err) })
|
|
5636
|
+
});
|
|
5637
|
+
setPhase("result");
|
|
5638
|
+
}
|
|
5639
|
+
}, [report]);
|
|
5640
|
+
const handleRemediate = useCallback6(async () => {
|
|
5641
|
+
if (!report) return;
|
|
5642
|
+
const actionable = report.violations.filter(
|
|
5643
|
+
(v) => v.type === "missing" || v.type === "wrong-version"
|
|
5644
|
+
);
|
|
5645
|
+
if (actionable.length === 0) return;
|
|
5646
|
+
setPhase("remediating");
|
|
5647
|
+
setStreamLines([]);
|
|
5648
|
+
setStreamRunning(true);
|
|
5649
|
+
const gen = remediateViolations(actionable, isPro());
|
|
5650
|
+
generatorRef.current = gen;
|
|
5651
|
+
try {
|
|
5652
|
+
for await (const line of gen) {
|
|
5653
|
+
if (!mountedRef.current) break;
|
|
5654
|
+
setStreamLines((prev) => [...prev.slice(-99), line]);
|
|
5655
|
+
}
|
|
5656
|
+
if (mountedRef.current) {
|
|
5657
|
+
setResultMessage({ ok: true, text: t("compliance_remediate_success") });
|
|
5658
|
+
await runCheck(isPro());
|
|
5659
|
+
}
|
|
5660
|
+
} catch (err) {
|
|
5661
|
+
if (mountedRef.current) {
|
|
5662
|
+
setResultMessage({
|
|
5663
|
+
ok: false,
|
|
5664
|
+
text: t("compliance_remediate_error", { error: err instanceof Error ? err.message : String(err) })
|
|
5665
|
+
});
|
|
5666
|
+
}
|
|
5667
|
+
} finally {
|
|
5668
|
+
generatorRef.current = null;
|
|
5669
|
+
if (mountedRef.current) {
|
|
5670
|
+
setStreamRunning(false);
|
|
5671
|
+
setPhase("result");
|
|
5672
|
+
}
|
|
5673
|
+
}
|
|
5674
|
+
}, [report, isPro, runCheck]);
|
|
5675
|
+
useInput17((input, key) => {
|
|
5676
|
+
if (phase === "remediating" || phase === "importing") return;
|
|
5677
|
+
if (phase === "result") {
|
|
5678
|
+
if (key.escape || input === "r") {
|
|
5679
|
+
setPhase("overview");
|
|
5680
|
+
setResultMessage(null);
|
|
5681
|
+
}
|
|
5682
|
+
return;
|
|
5683
|
+
}
|
|
5684
|
+
if (input === "i") {
|
|
5685
|
+
setPhase("importing");
|
|
5686
|
+
return;
|
|
5687
|
+
}
|
|
5688
|
+
if (input === "r" && policy) {
|
|
5689
|
+
handleRecheck();
|
|
5690
|
+
return;
|
|
5691
|
+
}
|
|
5692
|
+
if (input === "e" && report) {
|
|
5693
|
+
void handleExport();
|
|
5694
|
+
return;
|
|
5695
|
+
}
|
|
5696
|
+
if (input === "c" && report) {
|
|
5697
|
+
const actionable = report.violations.filter(
|
|
5698
|
+
(v) => v.type === "missing" || v.type === "wrong-version"
|
|
5699
|
+
);
|
|
5700
|
+
if (actionable.length > 0) {
|
|
5701
|
+
void handleRemediate();
|
|
5702
|
+
}
|
|
5703
|
+
return;
|
|
5704
|
+
}
|
|
5705
|
+
});
|
|
5706
|
+
if (phase === "remediating" || loading && phase !== "importing") {
|
|
5707
|
+
if (phase === "remediating") {
|
|
5708
|
+
return /* @__PURE__ */ jsxs33(Box32, { flexDirection: "column", children: [
|
|
5709
|
+
/* @__PURE__ */ jsx34(SectionHeader, { emoji: "\u{1F50D}", title: t("compliance_title"), gradient: GRADIENTS.gold }),
|
|
5710
|
+
/* @__PURE__ */ jsx34(Box32, { marginTop: 1, children: /* @__PURE__ */ jsx34(ProgressLog, { lines: streamLines, isRunning: streamRunning, title: t("compliance_remediating") }) })
|
|
5711
|
+
] });
|
|
5712
|
+
}
|
|
5713
|
+
return /* @__PURE__ */ jsx34(Loading, { message: t("compliance_title") });
|
|
5714
|
+
}
|
|
5715
|
+
if (phase === "result" && resultMessage) {
|
|
5716
|
+
return /* @__PURE__ */ jsxs33(Box32, { flexDirection: "column", marginTop: 1, children: [
|
|
5717
|
+
/* @__PURE__ */ jsx34(SectionHeader, { emoji: "\u{1F50D}", title: t("compliance_title"), gradient: GRADIENTS.gold }),
|
|
5718
|
+
/* @__PURE__ */ jsx34(Box32, { marginTop: 1, children: /* @__PURE__ */ jsx34(
|
|
5719
|
+
ResultBanner,
|
|
5720
|
+
{
|
|
5721
|
+
status: resultMessage.ok ? "success" : "error",
|
|
5722
|
+
message: resultMessage.text
|
|
5723
|
+
}
|
|
5724
|
+
) }),
|
|
5725
|
+
/* @__PURE__ */ jsx34(Box32, { marginTop: 1, children: /* @__PURE__ */ jsxs33(Text33, { color: COLORS.textSecondary, children: [
|
|
5726
|
+
"r:",
|
|
5727
|
+
t("hint_refresh"),
|
|
5728
|
+
" esc:",
|
|
5729
|
+
t("hint_back")
|
|
5730
|
+
] }) })
|
|
5731
|
+
] });
|
|
5732
|
+
}
|
|
5733
|
+
return /* @__PURE__ */ jsxs33(Box32, { flexDirection: "column", children: [
|
|
5734
|
+
/* @__PURE__ */ jsx34(SectionHeader, { emoji: "\u{1F50D}", title: t("compliance_title"), gradient: GRADIENTS.gold }),
|
|
5735
|
+
error && /* @__PURE__ */ jsx34(Box32, { marginTop: 1, children: /* @__PURE__ */ jsx34(ResultBanner, { status: "error", message: t("compliance_import_error", { error }) }) }),
|
|
5736
|
+
phase === "importing" && /* @__PURE__ */ jsxs33(Box32, { marginTop: 1, flexDirection: "column", children: [
|
|
5737
|
+
/* @__PURE__ */ jsx34(Text33, { color: COLORS.textSecondary, children: t("compliance_import_prompt") }),
|
|
5738
|
+
/* @__PURE__ */ jsx34(Box32, { marginTop: 1, children: /* @__PURE__ */ jsx34(
|
|
5739
|
+
TextInput7,
|
|
5740
|
+
{
|
|
5741
|
+
defaultValue: "",
|
|
5742
|
+
onSubmit: (val) => {
|
|
5743
|
+
void handleImportSubmit(val);
|
|
5744
|
+
}
|
|
5745
|
+
}
|
|
5746
|
+
) }),
|
|
5747
|
+
/* @__PURE__ */ jsx34(Box32, { marginTop: 1, children: /* @__PURE__ */ jsxs33(Text33, { color: COLORS.muted, dimColor: true, children: [
|
|
5748
|
+
"esc:",
|
|
5749
|
+
t("hint_back")
|
|
5750
|
+
] }) })
|
|
5751
|
+
] }),
|
|
5752
|
+
phase === "overview" && /* @__PURE__ */ jsxs33(Box32, { flexDirection: "column", marginTop: 1, children: [
|
|
5753
|
+
!policy ? /* @__PURE__ */ jsx34(Box32, { flexDirection: "column", children: /* @__PURE__ */ jsx34(Text33, { color: COLORS.textSecondary, children: t("compliance_no_policy") }) }) : /* @__PURE__ */ jsxs33(Box32, { flexDirection: "column", children: [
|
|
5754
|
+
/* @__PURE__ */ jsx34(Text33, { color: COLORS.textSecondary, bold: true, children: t("compliance_policy_by", { maintainer: policy.meta.maintainer }) }),
|
|
5755
|
+
report ? /* @__PURE__ */ jsxs33(Box32, { flexDirection: "column", marginTop: 1, children: [
|
|
5756
|
+
/* @__PURE__ */ jsx34(ComplianceScore, { report }),
|
|
5757
|
+
report.compliant ? /* @__PURE__ */ jsx34(ResultBanner, { status: "success", message: t("compliance_ok") }) : /* @__PURE__ */ jsx34(ViolationList, { violations: report.violations })
|
|
5758
|
+
] }) : /* @__PURE__ */ jsx34(Box32, { marginTop: 1, children: /* @__PURE__ */ jsx34(Text33, { color: COLORS.muted, dimColor: true, children: "Press r to run compliance check." }) })
|
|
5759
|
+
] }),
|
|
5760
|
+
/* @__PURE__ */ jsx34(Box32, { marginTop: 2, flexWrap: "wrap", children: /* @__PURE__ */ jsxs33(Text33, { color: COLORS.textSecondary, children: [
|
|
5761
|
+
"i:",
|
|
5762
|
+
t("hint_import"),
|
|
5763
|
+
policy && /* @__PURE__ */ jsxs33(Text33, { children: [
|
|
5764
|
+
" r:",
|
|
5765
|
+
t("hint_scan")
|
|
5766
|
+
] }),
|
|
5767
|
+
report && /* @__PURE__ */ jsxs33(Text33, { children: [
|
|
5768
|
+
" e:",
|
|
5769
|
+
t("hint_export")
|
|
5770
|
+
] }),
|
|
5771
|
+
report && report.violations.some((v) => v.type === "missing" || v.type === "wrong-version") && /* @__PURE__ */ jsxs33(Text33, { children: [
|
|
5772
|
+
" c:",
|
|
5773
|
+
t("hint_clean")
|
|
5774
|
+
] }),
|
|
5775
|
+
" q:",
|
|
5776
|
+
t("hint_quit")
|
|
5777
|
+
] }) })
|
|
5778
|
+
] })
|
|
5779
|
+
] });
|
|
5780
|
+
}
|
|
5781
|
+
|
|
5782
|
+
// src/app.tsx
|
|
5783
|
+
import { jsx as jsx35, jsxs as jsxs34 } from "react/jsx-runtime";
|
|
5784
|
+
function LicenseInitializer() {
|
|
5785
|
+
const initLicense = useLicenseStore((s) => s.initialize);
|
|
5786
|
+
useEffect19(() => {
|
|
5787
|
+
initLicense();
|
|
5788
|
+
}, []);
|
|
5789
|
+
return null;
|
|
5790
|
+
}
|
|
5791
|
+
function ViewRouter({ currentView }) {
|
|
5792
|
+
const isPro = useLicenseStore((s) => s.isPro);
|
|
5793
|
+
const isTeam = useLicenseStore((s) => s.isTeam);
|
|
5794
|
+
if (isProView(currentView) && !isPro()) {
|
|
5795
|
+
return /* @__PURE__ */ jsx35(UpgradePrompt, { viewId: currentView });
|
|
5796
|
+
}
|
|
5797
|
+
if (isTeamView(currentView) && !isTeam()) {
|
|
5798
|
+
return /* @__PURE__ */ jsx35(UpgradePrompt, { viewId: currentView });
|
|
5799
|
+
}
|
|
5800
|
+
switch (currentView) {
|
|
5801
|
+
case "dashboard":
|
|
5802
|
+
return /* @__PURE__ */ jsx35(DashboardView, {});
|
|
5803
|
+
case "installed":
|
|
5804
|
+
return /* @__PURE__ */ jsx35(InstalledView, {});
|
|
5805
|
+
case "search":
|
|
5806
|
+
return /* @__PURE__ */ jsx35(SearchView, {});
|
|
5807
|
+
case "outdated":
|
|
5808
|
+
return /* @__PURE__ */ jsx35(OutdatedView, {});
|
|
5809
|
+
case "package-info":
|
|
5810
|
+
return /* @__PURE__ */ jsx35(PackageInfoView, {});
|
|
5811
|
+
case "services":
|
|
5812
|
+
return /* @__PURE__ */ jsx35(ServicesView, {});
|
|
5813
|
+
case "doctor":
|
|
5814
|
+
return /* @__PURE__ */ jsx35(DoctorView, {});
|
|
5815
|
+
case "profiles":
|
|
5816
|
+
return /* @__PURE__ */ jsx35(ProfilesView, {});
|
|
5817
|
+
case "smart-cleanup":
|
|
5818
|
+
return /* @__PURE__ */ jsx35(SmartCleanupView, {});
|
|
5819
|
+
case "history":
|
|
5820
|
+
return /* @__PURE__ */ jsx35(HistoryView, {});
|
|
5821
|
+
case "rollback":
|
|
5822
|
+
return /* @__PURE__ */ jsx35(RollbackView, {});
|
|
5823
|
+
case "brewfile":
|
|
5824
|
+
return /* @__PURE__ */ jsx35(BrewfileView, {});
|
|
5825
|
+
case "sync":
|
|
5826
|
+
return /* @__PURE__ */ jsx35(SyncView, {});
|
|
5827
|
+
case "security-audit":
|
|
5828
|
+
return /* @__PURE__ */ jsx35(SecurityAuditView, {});
|
|
5829
|
+
case "compliance":
|
|
5830
|
+
return /* @__PURE__ */ jsx35(ComplianceView, {});
|
|
5831
|
+
case "account":
|
|
5832
|
+
return /* @__PURE__ */ jsx35(AccountView, {});
|
|
5833
|
+
}
|
|
5834
|
+
}
|
|
5835
|
+
function App() {
|
|
4329
5836
|
const { exit } = useApp();
|
|
4330
5837
|
const currentView = useNavigationStore((s) => s.currentView);
|
|
4331
5838
|
useGlobalKeyboard({ onQuit: exit });
|
|
4332
|
-
return /* @__PURE__ */
|
|
4333
|
-
/* @__PURE__ */
|
|
4334
|
-
/* @__PURE__ */
|
|
5839
|
+
return /* @__PURE__ */ jsxs34(AppLayout, { children: [
|
|
5840
|
+
/* @__PURE__ */ jsx35(LicenseInitializer, {}),
|
|
5841
|
+
/* @__PURE__ */ jsx35(ViewRouter, { currentView })
|
|
4335
5842
|
] });
|
|
4336
5843
|
}
|
|
4337
5844
|
|
|
4338
5845
|
// src/index.tsx
|
|
4339
|
-
import { jsx as
|
|
5846
|
+
import { jsx as jsx36 } from "react/jsx-runtime";
|
|
4340
5847
|
var [, , command, arg] = process.argv;
|
|
4341
5848
|
async function runCli() {
|
|
4342
5849
|
await ensureDataDirs();
|
|
@@ -4404,6 +5911,7 @@ async function runCli() {
|
|
|
4404
5911
|
if (command === "status") {
|
|
4405
5912
|
await useLicenseStore.getState().initialize();
|
|
4406
5913
|
const { status, license, degradation } = useLicenseStore.getState();
|
|
5914
|
+
const isPro = useLicenseStore.getState().isPro();
|
|
4407
5915
|
if (status === "free") {
|
|
4408
5916
|
console.log(t("cli_planFree"));
|
|
4409
5917
|
console.log(t("cli_upgradeHint"));
|
|
@@ -4427,12 +5935,53 @@ async function runCli() {
|
|
|
4427
5935
|
console.log(t("cli_revalidateHint"));
|
|
4428
5936
|
}
|
|
4429
5937
|
}
|
|
5938
|
+
if (isPro) {
|
|
5939
|
+
try {
|
|
5940
|
+
const { loadSnapshots: loadSnapshots2 } = await import("./snapshot-RAPGMAJF.js");
|
|
5941
|
+
const snapshots = await loadSnapshots2();
|
|
5942
|
+
if (snapshots.length > 0) {
|
|
5943
|
+
const latest = snapshots[0];
|
|
5944
|
+
console.log(`
|
|
5945
|
+
Snapshots: ${snapshots.length} (latest: ${latest ? formatDate(latest.capturedAt) : "\u2014"})`);
|
|
5946
|
+
} else {
|
|
5947
|
+
console.log("\nSnapshots: none");
|
|
5948
|
+
}
|
|
5949
|
+
} catch {
|
|
5950
|
+
}
|
|
5951
|
+
try {
|
|
5952
|
+
const { loadBrewfile: loadBrewfile2, computeDrift: computeDrift2 } = await import("./brewfile-manager-3SERRYNC.js");
|
|
5953
|
+
const schema = await loadBrewfile2();
|
|
5954
|
+
if (schema) {
|
|
5955
|
+
const drift = await computeDrift2(schema);
|
|
5956
|
+
console.log(`Brewfile: ${drift.score}% compliant (${drift.missingPackages.length} missing, ${drift.extraPackages.length} extra)`);
|
|
5957
|
+
}
|
|
5958
|
+
} catch {
|
|
5959
|
+
}
|
|
5960
|
+
try {
|
|
5961
|
+
const { loadSyncConfig: loadSyncConfig2 } = await import("./sync-engine-4ERSW4EQ.js");
|
|
5962
|
+
const syncConfig = await loadSyncConfig2();
|
|
5963
|
+
if (syncConfig?.lastSync) {
|
|
5964
|
+
console.log(`Sync: last sync ${formatDate(syncConfig.lastSync)}`);
|
|
5965
|
+
}
|
|
5966
|
+
} catch {
|
|
5967
|
+
}
|
|
5968
|
+
try {
|
|
5969
|
+
const { loadPolicy: loadPolicy2 } = await import("./policy-io-EECGRKNA.js");
|
|
5970
|
+
const { checkCompliance: checkCompliance2 } = await import("./compliance-checker-X7P623UF.js");
|
|
5971
|
+
const policy = await loadPolicy2(`${process.env["HOME"] ?? "~"}/.brew-tui/policy.yaml`).catch(() => null);
|
|
5972
|
+
if (policy) {
|
|
5973
|
+
const report = await checkCompliance2(policy, true);
|
|
5974
|
+
console.log(`Compliance: ${report.score}% (${report.violations.length} violation(s))`);
|
|
5975
|
+
}
|
|
5976
|
+
} catch {
|
|
5977
|
+
}
|
|
5978
|
+
}
|
|
4430
5979
|
return;
|
|
4431
5980
|
}
|
|
4432
5981
|
if (command === "install-brewbar") {
|
|
4433
5982
|
await useLicenseStore.getState().initialize();
|
|
4434
5983
|
const isPro = useLicenseStore.getState().isPro();
|
|
4435
|
-
const { installBrewBar } = await import("./brewbar-installer-
|
|
5984
|
+
const { installBrewBar } = await import("./brewbar-installer-ZEMXNDHP.js");
|
|
4436
5985
|
try {
|
|
4437
5986
|
await installBrewBar(isPro, arg === "--force");
|
|
4438
5987
|
console.log(t("cli_brewbarInstalled"));
|
|
@@ -4443,7 +5992,7 @@ async function runCli() {
|
|
|
4443
5992
|
return;
|
|
4444
5993
|
}
|
|
4445
5994
|
if (command === "uninstall-brewbar") {
|
|
4446
|
-
const { uninstallBrewBar } = await import("./brewbar-installer-
|
|
5995
|
+
const { uninstallBrewBar } = await import("./brewbar-installer-ZEMXNDHP.js");
|
|
4447
5996
|
try {
|
|
4448
5997
|
await uninstallBrewBar();
|
|
4449
5998
|
console.log(t("cli_brewbarUninstalled"));
|
|
@@ -4466,14 +6015,15 @@ async function runCli() {
|
|
|
4466
6015
|
return;
|
|
4467
6016
|
}
|
|
4468
6017
|
await ensureBrewBarRunning();
|
|
6018
|
+
process.env.BREW_TUI_TUI_MODE = "1";
|
|
4469
6019
|
process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
|
4470
|
-
render(/* @__PURE__ */
|
|
6020
|
+
render(/* @__PURE__ */ jsx36(App, {}));
|
|
4471
6021
|
}
|
|
4472
6022
|
async function ensureBrewBarRunning() {
|
|
4473
6023
|
if (process.platform !== "darwin") return;
|
|
4474
6024
|
await useLicenseStore.getState().initialize();
|
|
4475
6025
|
if (!useLicenseStore.getState().isPro()) return;
|
|
4476
|
-
const { isBrewBarInstalled, installBrewBar, launchBrewBar } = await import("./brewbar-installer-
|
|
6026
|
+
const { isBrewBarInstalled, installBrewBar, launchBrewBar } = await import("./brewbar-installer-ZEMXNDHP.js");
|
|
4477
6027
|
try {
|
|
4478
6028
|
if (!await isBrewBarInstalled()) {
|
|
4479
6029
|
console.log(t("cli_brewbarInstalling"));
|