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