brew-tui 0.4.1 → 0.5.0

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