brew-tui 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  loadSyncConfig,
13
13
  readSyncEnvelope,
14
14
  sync
15
- } from "./chunk-42URLVAJ.js";
15
+ } from "./chunk-AIAZQJKL.js";
16
16
  import {
17
17
  checkCompliance
18
18
  } from "./chunk-U2DRWB7A.js";
@@ -40,12 +40,13 @@ import {
40
40
  ensureDataDirs
41
41
  } from "./chunk-UWS4A4F5.js";
42
42
  import {
43
+ fetchWithRetry,
43
44
  fetchWithTimeout,
44
45
  getLocale,
45
46
  t,
46
47
  tp,
47
48
  useLocaleStore
48
- } from "./chunk-3BPHXV35.js";
49
+ } from "./chunk-MSXH66I2.js";
49
50
  import {
50
51
  logger
51
52
  } from "./chunk-KDHEUNRI.js";
@@ -151,7 +152,8 @@ var COLORS = {
151
152
  purple: "#A855F7",
152
153
  blue: "#3B82F6",
153
154
  lavender: "#C4B5FD",
154
- border: "#4B5563"
155
+ border: "#4B5563",
156
+ white: "#FFFFFF"
155
157
  };
156
158
 
157
159
  // src/utils/gradient.tsx
@@ -199,7 +201,8 @@ var GRADIENTS = {
199
201
  emerald: ["#22C55E", "#2DD4BF", "#06B6D4"],
200
202
  fire: ["#EF4444", "#F59E0B", "#FFD700"],
201
203
  version: ["#EF4444", "#9CA3AF", "#2DD4BF"],
202
- pro: ["#FF6B2B", "#FFD700", "#FF6B2B"]
204
+ pro: ["#FF6B2B", "#FFD700", "#FF6B2B"],
205
+ darkGold: ["#B8860B", "#8B6914", "#6B4F10"]
203
206
  };
204
207
 
205
208
  // src/components/layout/header.tsx
@@ -285,7 +288,7 @@ function MenuItem({ view, currentView }) {
285
288
  "\u25B6",
286
289
  " "
287
290
  ] }) : /* @__PURE__ */ jsx2(Text2, { children: " " }),
288
- /* @__PURE__ */ jsx2(Text2, { bold: true, color: key ? "#FFFFFF" : COLORS.textSecondary, children: indicator }),
291
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: key ? COLORS.white : COLORS.textSecondary, children: indicator }),
289
292
  /* @__PURE__ */ jsxs(Text2, { bold: isActive, underline: isActive, color: isActive ? COLORS.success : isAccount ? COLORS.gold : COLORS.textSecondary, children: [
290
293
  " ",
291
294
  viewLabel
@@ -306,7 +309,7 @@ function Header() {
306
309
  const isNarrow = cols < 95;
307
310
  const logoBlock = /* @__PURE__ */ jsx2(Box, { flexDirection: "column", flexShrink: 0, children: LOGO_BREW.map((brew, i) => /* @__PURE__ */ jsxs(Box, { children: [
308
311
  /* @__PURE__ */ jsx2(GradientText, { colors: GRADIENTS.gold, children: brew }),
309
- /* @__PURE__ */ jsx2(GradientText, { colors: ["#B8860B", "#8B6914", "#6B4F10"], children: LOGO_TUI[i] })
312
+ /* @__PURE__ */ jsx2(GradientText, { colors: GRADIENTS.darkGold, children: LOGO_TUI[i] })
310
313
  ] }, i)) });
311
314
  const menuBlock = /* @__PURE__ */ jsxs(Box, { borderStyle: "round", borderColor: COLORS.lavender, paddingX: 1, flexDirection: "column", alignSelf: isNarrow ? "flex-start" : "center", children: [
312
315
  /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
@@ -314,7 +317,7 @@ function Header() {
314
317
  /* @__PURE__ */ jsx2(Box, { flexDirection: "column", marginLeft: 2, children: COL2_VIEWS.map((view) => /* @__PURE__ */ jsx2(MenuItem, { view, currentView }, view)) })
315
318
  ] }),
316
319
  /* @__PURE__ */ jsxs(Box, { borderStyle: "single", borderTop: true, borderBottom: false, borderLeft: false, borderRight: false, borderColor: COLORS.lavender, marginTop: 0, children: [
317
- /* @__PURE__ */ jsx2(Text2, { bold: true, color: "#FFFFFF", children: "S" }),
320
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: COLORS.white, children: "S" }),
318
321
  /* @__PURE__ */ jsxs(Text2, { color: COLORS.textSecondary, children: [
319
322
  " ",
320
323
  t("hint_search")
@@ -324,7 +327,7 @@ function Header() {
324
327
  "\u2503",
325
328
  " "
326
329
  ] }),
327
- /* @__PURE__ */ jsx2(Text2, { bold: true, color: "#FFFFFF", children: "L" }),
330
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: COLORS.white, children: "L" }),
328
331
  /* @__PURE__ */ jsxs(Text2, { color: COLORS.textSecondary, children: [
329
332
  " ",
330
333
  t("hint_lang")
@@ -349,19 +352,19 @@ import { Box as Box2, Text as Text3 } from "ink";
349
352
  import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
350
353
  var VIEW_HINT_DEFS = {
351
354
  dashboard: [["1-9,0", "hint_navigate"], ["S", "hint_search"], ["tab", "hint_next"], ["q", "hint_quit"]],
352
- installed: [["/", "hint_filter"], ["enter", "hint_info"], ["u", "hint_uninstall"], ["S", "hint_search"], ["q", "hint_quit"]],
355
+ installed: [["/", "hint_filter"], ["enter", "hint_info"], ["u", "hint_uninstall"], ["f", "hint_switchTab"], ["S", "hint_search"], ["q", "hint_quit"]],
353
356
  search: [["hint_typeToSearch"], ["enter", "hint_details"], ["i", "hint_install"], ["esc", "hint_back"], ["q", "hint_quit"]],
354
- outdated: [["enter", "hint_upgrade"], ["A", "hint_upgradeAll"], ["p", "hint_pin"], ["S", "hint_search"], ["q", "hint_quit"]],
357
+ outdated: [["enter", "hint_upgrade"], ["A", "hint_upgradeAll"], ["p", "hint_pin"], ["r", "hint_refresh"], ["S", "hint_search"], ["q", "hint_quit"]],
355
358
  "package-info": [["i", "hint_install"], ["u", "hint_uninstall"], ["U", "hint_upgrade"], ["esc", "hint_back"], ["q", "hint_quit"]],
356
359
  services: [["s", "hint_start"], ["x", "hint_stop"], ["R", "hint_restart"], ["r", "hint_refresh"], ["S", "hint_search"], ["q", "hint_quit"]],
357
360
  doctor: [["r", "hint_refresh"], ["S", "hint_search"], ["tab", "hint_next"], ["q", "hint_quit"]],
358
361
  profiles: [["n", "hint_new"], ["enter", "hint_details"], ["e", "hint_edit"], ["i", "hint_import"], ["d", "hint_delete"], ["q", "hint_quit"]],
359
- "smart-cleanup": [["enter", "hint_toggle"], ["c", "hint_clean"], ["r", "hint_refresh"], ["S", "hint_search"], ["q", "hint_quit"]],
362
+ "smart-cleanup": [["enter", "hint_toggle"], ["a", "hint_all"], ["c", "hint_clean"], ["F", "hint_force"], ["r", "hint_refresh"], ["S", "hint_search"], ["q", "hint_quit"]],
360
363
  history: [["/", "hint_search"], ["enter", "hint_replay"], ["f", "hint_filter"], ["c", "hint_clear"], ["q", "hint_quit"]],
361
364
  "security-audit": [["r", "hint_scan"], ["enter", "hint_details"], ["u", "hint_upgrade"], ["S", "hint_search"], ["q", "hint_quit"]],
362
365
  rollback: [["j/k", "hint_navigate"], ["enter", "hint_select"], ["r", "hint_rollback_confirm"], ["esc", "hint_back"], ["q", "hint_quit"]],
363
366
  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"]],
367
+ sync: [["s", "hint_sync"], ["r", "hint_refresh"], ["c", "hint_conflict"], ["l", "hint_useLocal"], ["esc", "hint_back"], ["q", "hint_quit"]],
365
368
  compliance: [["r", "hint_scan"], ["i", "hint_import"], ["e", "hint_export"], ["c", "hint_clean"], ["q", "hint_quit"]],
366
369
  account: [["p", "hint_promo"], ["d", "hint_deactivate"], ["S", "hint_search"], ["q", "hint_quit"]]
367
370
  };
@@ -454,7 +457,7 @@ function validateApiUrl(url) {
454
457
  async function post(endpoint, body, expectEmpty = false) {
455
458
  const url = `${BASE_URL}/${endpoint}`;
456
459
  validateApiUrl(url);
457
- const res = await fetchWithTimeout(url, {
460
+ const res = await fetchWithRetry(url, {
458
461
  method: "POST",
459
462
  headers: { "Content-Type": "application/json" },
460
463
  body: JSON.stringify(body)
@@ -543,15 +546,6 @@ async function deactivateLicense(key, instanceId) {
543
546
  }
544
547
 
545
548
  // src/lib/license/license-manager.ts
546
- var BUILTIN_ACCOUNTS = {
547
- "admin@molinesdesigns.com": "pro",
548
- "team@molinesdesigns.com": "team",
549
- // Team tier test/admin account
550
- "artax1983@icloud.com": "free"
551
- };
552
- function getBuiltinAccountType(email) {
553
- return BUILTIN_ACCOUNTS[email] ?? null;
554
- }
555
549
  var REVALIDATION_INTERVAL_MS = 24 * 60 * 60 * 1e3;
556
550
  var GRACE_PERIOD_MS = 7 * 24 * 60 * 60 * 1e3;
557
551
  var ACTIVATION_COOLDOWN_MS = 3e4;
@@ -630,9 +624,9 @@ function isEncryptedLicenseFile(obj) {
630
624
  async function getMachineId2() {
631
625
  try {
632
626
  const { readFile: readMachineId } = await import("fs/promises");
633
- const { join: join6 } = await import("path");
634
- const { homedir: homedir3 } = await import("os");
635
- const machineIdPath = join6(homedir3(), ".brew-tui", "machine-id");
627
+ const { join: join7 } = await import("path");
628
+ const { homedir: homedir4 } = await import("os");
629
+ const machineIdPath = join7(homedir4(), ".brew-tui", "machine-id");
636
630
  return (await readMachineId(machineIdPath, "utf-8")).trim() || null;
637
631
  } catch {
638
632
  return null;
@@ -801,6 +795,14 @@ function initStoreIntegrity(store) {
801
795
  _originalIsPro = store.getState().isPro;
802
796
  _originalGetState = store.getState;
803
797
  }
798
+ function verifyStoreIntegrity() {
799
+ if (!_storeApi || !_originalIsPro || !_originalGetState) return false;
800
+ const state = _storeApi.getState();
801
+ if (state.isPro !== _originalIsPro) return false;
802
+ if (_storeApi.getState !== _originalGetState) return false;
803
+ if (state.status === "free" && state.isPro()) return false;
804
+ return true;
805
+ }
804
806
 
805
807
  // src/stores/license-store.ts
806
808
  var REVALIDATION_CHECK_MS = 60 * 60 * 1e3;
@@ -829,19 +831,6 @@ var useLicenseStore = create2((set, get) => ({
829
831
  set({ status: "free", license: null, degradation: "none" });
830
832
  return;
831
833
  }
832
- const builtinType = getBuiltinAccountType(license.customerEmail);
833
- if (builtinType === "pro") {
834
- set({ status: "pro", license: { ...license, status: "active" }, degradation: "none" });
835
- return;
836
- }
837
- if (builtinType === "team") {
838
- set({ status: "team", license: { ...license, status: "active", plan: "team" }, degradation: "none" });
839
- return;
840
- }
841
- if (builtinType === "free") {
842
- set({ status: "free", license: null, degradation: "none" });
843
- return;
844
- }
845
834
  if (isExpired(license)) {
846
835
  set({ status: "expired", license, degradation: "expired" });
847
836
  return;
@@ -1254,7 +1243,7 @@ async function getInstalled() {
1254
1243
  return parseInstalledJson(raw);
1255
1244
  }
1256
1245
  async function getOutdated() {
1257
- const raw = await execBrew(["outdated", "--json=v2"]);
1246
+ const raw = await execBrew(["outdated", "--json=v2", "--greedy"]);
1258
1247
  return parseOutdatedJson(raw);
1259
1248
  }
1260
1249
  async function getServices() {
@@ -1535,11 +1524,13 @@ function getFixedVersion(vuln) {
1535
1524
  }
1536
1525
  var BATCH_SIZE = 100;
1537
1526
  async function queryBatch(packages, queries) {
1538
- const res = await fetchWithTimeout(OSV_BATCH_URL, {
1527
+ const res = await fetchWithRetry(OSV_BATCH_URL, {
1539
1528
  method: "POST",
1540
1529
  headers: { "Content-Type": "application/json" },
1541
1530
  body: JSON.stringify({ queries })
1542
- }, 15e3);
1531
+ }, 15e3, {
1532
+ retryOn: (r) => r.status >= 500 && r.status < 600
1533
+ });
1543
1534
  if (!res.ok) {
1544
1535
  if (res.status === 400 && queries.length > 1) {
1545
1536
  return queryOneByOne(packages);
@@ -1686,6 +1677,93 @@ async function runSecurityAudit(isPro, formulae, casks) {
1686
1677
  };
1687
1678
  }
1688
1679
 
1680
+ // src/lib/license/anti-debug.ts
1681
+ import inspector from "inspector";
1682
+ function isDebuggerAttached() {
1683
+ if (false) return false;
1684
+ if (inspector.url()) return true;
1685
+ if (process.execArgv.some((a) => a.includes("--inspect") || a.includes("--debug"))) return true;
1686
+ if (process.env.NODE_OPTIONS?.includes("--inspect")) return true;
1687
+ return false;
1688
+ }
1689
+
1690
+ // src/lib/license/canary.ts
1691
+ var _canaryTripped = false;
1692
+ function isProUnlocked() {
1693
+ return false;
1694
+ }
1695
+ function hasProAccess() {
1696
+ return false;
1697
+ }
1698
+ function isLicenseValid() {
1699
+ return false;
1700
+ }
1701
+ function checkCanaries() {
1702
+ if (isProUnlocked()) {
1703
+ _canaryTripped = true;
1704
+ return false;
1705
+ }
1706
+ if (hasProAccess()) {
1707
+ _canaryTripped = true;
1708
+ return false;
1709
+ }
1710
+ if (isLicenseValid()) {
1711
+ _canaryTripped = true;
1712
+ return false;
1713
+ }
1714
+ return !_canaryTripped;
1715
+ }
1716
+
1717
+ // src/lib/license/integrity.ts
1718
+ import { readFileSync } from "fs";
1719
+ import { createHash } from "crypto";
1720
+ import { fileURLToPath } from "url";
1721
+ var _baselineHash = null;
1722
+ var _isProduction = true;
1723
+ function _captureBaseline() {
1724
+ try {
1725
+ const bundlePath = fileURLToPath(import.meta.url);
1726
+ const content = readFileSync(bundlePath, "utf-8");
1727
+ return createHash("sha256").update(content).digest("hex");
1728
+ } catch {
1729
+ return null;
1730
+ }
1731
+ }
1732
+ _baselineHash = _captureBaseline();
1733
+ function checkBundleIntegrity() {
1734
+ if (_baselineHash === null) {
1735
+ return !_isProduction;
1736
+ }
1737
+ try {
1738
+ const bundlePath = fileURLToPath(import.meta.url);
1739
+ const content = readFileSync(bundlePath, "utf-8");
1740
+ const current = createHash("sha256").update(content).digest("hex");
1741
+ return current === _baselineHash;
1742
+ } catch {
1743
+ return !_isProduction;
1744
+ }
1745
+ }
1746
+
1747
+ // src/lib/license/pro-guard.ts
1748
+ var _P = String.fromCharCode(112, 114, 111);
1749
+ function _verify(status) {
1750
+ return status === _P;
1751
+ }
1752
+ function verifyPro(license, status) {
1753
+ if (isDebuggerAttached()) return false;
1754
+ if (!checkBundleIntegrity()) return false;
1755
+ if (!verifyStoreIntegrity()) return false;
1756
+ if (!checkCanaries()) return false;
1757
+ const directCheck = status === "pro";
1758
+ const indirectCheck = _verify(status);
1759
+ if (!directCheck || !indirectCheck) return false;
1760
+ if (license) {
1761
+ const level = getDegradationLevel(license);
1762
+ if (level === "expired" || level === "limited") return false;
1763
+ }
1764
+ return true;
1765
+ }
1766
+
1689
1767
  // src/stores/security-store.ts
1690
1768
  var CACHE_TTL_MS = 30 * 60 * 1e3;
1691
1769
  var useSecurityStore = create5((set, get) => ({
@@ -1705,7 +1783,8 @@ var useSecurityStore = create5((set, get) => ({
1705
1783
  await brewState.fetchInstalled();
1706
1784
  }
1707
1785
  const { formulae, casks } = useBrewStore.getState();
1708
- const isPro = useLicenseStore.getState().isPro();
1786
+ const { license, status } = useLicenseStore.getState();
1787
+ const isPro = verifyPro(license, status);
1709
1788
  const result = await runSecurityAudit(isPro, formulae, casks);
1710
1789
  set({ summary: result, loading: false, cachedAt: Date.now() });
1711
1790
  } catch (err) {
@@ -2199,8 +2278,7 @@ function useBrewStream() {
2199
2278
  const MUTATING_COMMANDS = /* @__PURE__ */ new Set(["install", "uninstall", "upgrade", "pin", "unpin", "tap", "untap"]);
2200
2279
  if (!cancelRef.current && MUTATING_COMMANDS.has(args[0] ?? "")) {
2201
2280
  void import("./snapshot-RAPGMAJF.js").then(({ captureSnapshot: captureSnapshot2, saveSnapshot: saveSnapshot2 }) => {
2202
- captureSnapshot2().then((s) => saveSnapshot2(s)).catch(() => {
2203
- });
2281
+ captureSnapshot2().then((s) => saveSnapshot2(s)).catch((err) => logger.warn("snapshot: capture/save failed", { error: String(err) }));
2204
2282
  });
2205
2283
  }
2206
2284
  }, []);
@@ -2312,7 +2390,11 @@ function SelectableRow({ isCurrent, children, gap = 1 }) {
2312
2390
  // src/views/installed.tsx
2313
2391
  import { jsx as jsx16, jsxs as jsxs15 } from "react/jsx-runtime";
2314
2392
  function InstalledView() {
2315
- const { formulae, casks, loading, errors, fetchInstalled } = useBrewStore();
2393
+ const formulae = useBrewStore((s) => s.formulae);
2394
+ const casks = useBrewStore((s) => s.casks);
2395
+ const loading = useBrewStore((s) => s.loading);
2396
+ const errors = useBrewStore((s) => s.errors);
2397
+ const fetchInstalled = useBrewStore((s) => s.fetchInstalled);
2316
2398
  const navigate = useNavigationStore((s) => s.navigate);
2317
2399
  const selectPackage = useNavigationStore((s) => s.selectPackage);
2318
2400
  const [filter, setFilter] = useState3("");
@@ -3420,7 +3502,8 @@ async function* importProfile(isPro, profile) {
3420
3502
 
3421
3503
  // src/stores/profile-store.ts
3422
3504
  function getIsPro() {
3423
- return useLicenseStore.getState().isPro();
3505
+ const { license, status } = useLicenseStore.getState();
3506
+ return verifyPro(license, status);
3424
3507
  }
3425
3508
  var useProfileStore = create9((set) => ({
3426
3509
  profileNames: [],
@@ -3907,7 +3990,8 @@ var useCleanupStore = create10((set, get) => ({
3907
3990
  await brewState.fetchLeaves();
3908
3991
  }
3909
3992
  const { formulae, leaves } = useBrewStore.getState();
3910
- const isPro = useLicenseStore.getState().isPro();
3993
+ const { license, status } = useLicenseStore.getState();
3994
+ const isPro = verifyPro(license, status);
3911
3995
  const summary = await analyzeCleanup(isPro, formulae, leaves);
3912
3996
  set({ summary, selected: /* @__PURE__ */ new Set(), loading: false });
3913
3997
  } catch (err) {
@@ -4068,6 +4152,10 @@ import { Box as Box26, Text as Text27, useInput as useInput11, useStdout as useS
4068
4152
 
4069
4153
  // src/stores/history-store.ts
4070
4154
  import { create as create11 } from "zustand";
4155
+ function getStrongIsPro() {
4156
+ const { license, status } = useLicenseStore.getState();
4157
+ return verifyPro(license, status);
4158
+ }
4071
4159
  var useHistoryStore = create11((set) => ({
4072
4160
  entries: [],
4073
4161
  loading: false,
@@ -4075,7 +4163,7 @@ var useHistoryStore = create11((set) => ({
4075
4163
  fetchHistory: async () => {
4076
4164
  set({ loading: true, error: null });
4077
4165
  try {
4078
- const isPro = useLicenseStore.getState().isPro();
4166
+ const isPro = getStrongIsPro();
4079
4167
  const entries = await loadHistory(isPro);
4080
4168
  set({ entries, loading: false });
4081
4169
  } catch (err) {
@@ -4083,13 +4171,13 @@ var useHistoryStore = create11((set) => ({
4083
4171
  }
4084
4172
  },
4085
4173
  logAction: async (action, packageName, success, error = null) => {
4086
- const isPro = useLicenseStore.getState().isPro();
4174
+ const isPro = getStrongIsPro();
4087
4175
  await appendEntry(isPro, action, packageName, success, error);
4088
4176
  const entries = await loadHistory(isPro);
4089
4177
  set({ entries });
4090
4178
  },
4091
4179
  clearHistory: async () => {
4092
- const isPro = useLicenseStore.getState().isPro();
4180
+ const isPro = getStrongIsPro();
4093
4181
  await clearHistory(isPro);
4094
4182
  set({ entries: [] });
4095
4183
  }
@@ -4416,6 +4504,8 @@ var PROMO_API_URL = "https://api.molinesdesigns.com/api/promo";
4416
4504
  async function redeemPromoCode(code) {
4417
4505
  const normalized = code.trim().toUpperCase();
4418
4506
  const machineId = await getMachineId3();
4507
+ let serverExpiresAt;
4508
+ let serverType;
4419
4509
  try {
4420
4510
  const res = await fetchWithTimeout(`${PROMO_API_URL}/redeem`, {
4421
4511
  method: "POST",
@@ -4427,8 +4517,17 @@ async function redeemPromoCode(code) {
4427
4517
  return { success: false, error: body.error ?? "Invalid or expired promo code" };
4428
4518
  }
4429
4519
  const data = await res.json();
4430
- var serverExpiresAt = data.data.expiresAt;
4431
- var serverType = data.data.type;
4520
+ const inner = data?.data;
4521
+ if (!inner || typeof inner !== "object" || typeof inner.expiresAt !== "string" || typeof inner.type !== "string") {
4522
+ return { success: false, error: "Unexpected promo redeem response" };
4523
+ }
4524
+ const expiresAt = inner.expiresAt;
4525
+ const type = inner.type;
4526
+ if (type !== "trial" && type !== "discount" && type !== "full") {
4527
+ return { success: false, error: "Unsupported promo type" };
4528
+ }
4529
+ serverExpiresAt = expiresAt;
4530
+ serverType = type;
4432
4531
  } catch (err) {
4433
4532
  logger.error("Promo redeem failed", { error: String(err) });
4434
4533
  return { success: false, error: "Could not reach promo server. Check your connection." };
@@ -4605,7 +4704,7 @@ function AccountView() {
4605
4704
  /* @__PURE__ */ jsx30(Box28, { marginTop: 1, children: /* @__PURE__ */ jsxs29(Text29, { color: COLORS.textSecondary, children: [
4606
4705
  status === "pro" ? `d ${t("hint_deactivate")}` : "",
4607
4706
  " ",
4608
- t("app_version", { version: "0.5.2" })
4707
+ t("app_version", { version: "0.6.0" })
4609
4708
  ] }) })
4610
4709
  ] });
4611
4710
  }
@@ -5763,7 +5862,7 @@ function ComplianceView() {
5763
5862
  report ? /* @__PURE__ */ jsxs33(Box32, { flexDirection: "column", marginTop: 1, children: [
5764
5863
  /* @__PURE__ */ jsx34(ComplianceScore, { report }),
5765
5864
  report.compliant ? /* @__PURE__ */ jsx34(ResultBanner, { status: "success", message: t("compliance_ok") }) : /* @__PURE__ */ jsx34(ViolationList, { violations: report.violations })
5766
- ] }) : /* @__PURE__ */ jsx34(Box32, { marginTop: 1, children: /* @__PURE__ */ jsx34(Text33, { color: COLORS.muted, dimColor: true, children: "Press r to run compliance check." }) })
5865
+ ] }) : /* @__PURE__ */ jsx34(Box32, { marginTop: 1, children: /* @__PURE__ */ jsx34(Text33, { color: COLORS.muted, dimColor: true, children: t("compliance_press_r_hint") }) })
5767
5866
  ] }),
5768
5867
  /* @__PURE__ */ jsx34(Box32, { marginTop: 2, flexWrap: "wrap", children: /* @__PURE__ */ jsxs33(Text33, { color: COLORS.textSecondary, children: [
5769
5868
  "i:",
@@ -5850,11 +5949,127 @@ function App() {
5850
5949
  ] });
5851
5950
  }
5852
5951
 
5952
+ // src/lib/crash-reporter.ts
5953
+ import { readFile as readFile5, writeFile as writeFile5, mkdir as mkdir3 } from "fs/promises";
5954
+ import { randomUUID as randomUUID3 } from "crypto";
5955
+ import { homedir as homedir3, platform, release, arch } from "os";
5956
+ import { join as join6 } from "path";
5957
+ var ENDPOINT_ENV = "BREW_TUI_CRASH_ENDPOINT";
5958
+ var TOKEN_ENV = "BREW_TUI_CRASH_TOKEN";
5959
+ var CONFIG_PATH = join6(homedir3(), ".brew-tui", "crash-reporter.json");
5960
+ var MACHINE_ID_PATH3 = join6(homedir3(), ".brew-tui", "machine-id");
5961
+ var POST_TIMEOUT_MS = 5e3;
5962
+ var _installed = false;
5963
+ async function loadConfigFromDisk() {
5964
+ try {
5965
+ const raw = await readFile5(CONFIG_PATH, "utf-8");
5966
+ const parsed = JSON.parse(raw);
5967
+ return {
5968
+ endpoint: typeof parsed.endpoint === "string" ? parsed.endpoint : null,
5969
+ token: typeof parsed.token === "string" ? parsed.token : null,
5970
+ enabled: parsed.enabled === true
5971
+ };
5972
+ } catch {
5973
+ return { endpoint: null, token: null, enabled: false };
5974
+ }
5975
+ }
5976
+ async function getMachineId4() {
5977
+ try {
5978
+ const id2 = (await readFile5(MACHINE_ID_PATH3, "utf-8")).trim();
5979
+ if (id2) return id2;
5980
+ } catch {
5981
+ }
5982
+ const id = randomUUID3();
5983
+ try {
5984
+ await mkdir3(join6(homedir3(), ".brew-tui"), { recursive: true, mode: 448 });
5985
+ await writeFile5(MACHINE_ID_PATH3, id, { encoding: "utf-8", mode: 384 });
5986
+ } catch {
5987
+ }
5988
+ return id;
5989
+ }
5990
+ async function resolveConfig() {
5991
+ const envEndpoint = process.env[ENDPOINT_ENV]?.trim();
5992
+ const envToken = process.env[TOKEN_ENV]?.trim();
5993
+ if (envEndpoint) {
5994
+ return { endpoint: envEndpoint, token: envToken ?? null, enabled: true };
5995
+ }
5996
+ return loadConfigFromDisk();
5997
+ }
5998
+ function isHttpsOrLocal(url) {
5999
+ try {
6000
+ const parsed = new URL(url);
6001
+ if (parsed.protocol === "https:") return true;
6002
+ if (parsed.protocol === "http:") {
6003
+ const host = parsed.hostname;
6004
+ return /^(localhost|127\.\d+\.\d+\.\d+|10\.|192\.168\.|172\.(1[6-9]|2\d|3[0-1])\.|::1$)/i.test(host);
6005
+ }
6006
+ return false;
6007
+ } catch {
6008
+ return false;
6009
+ }
6010
+ }
6011
+ async function postReport(report, config) {
6012
+ if (!config.endpoint || !isHttpsOrLocal(config.endpoint)) return;
6013
+ try {
6014
+ await fetch(config.endpoint, {
6015
+ method: "POST",
6016
+ headers: {
6017
+ "Content-Type": "application/json",
6018
+ ...config.token ? { "Authorization": `Bearer ${config.token}` } : {}
6019
+ },
6020
+ body: JSON.stringify(report),
6021
+ signal: AbortSignal.timeout(POST_TIMEOUT_MS)
6022
+ });
6023
+ } catch (err) {
6024
+ logger.warn("crash-reporter: POST failed", { error: String(err) });
6025
+ }
6026
+ }
6027
+ function buildReport(level, err, context, machineId, version) {
6028
+ const message = err instanceof Error ? err.message : String(err);
6029
+ const stack = err instanceof Error && typeof err.stack === "string" ? err.stack : null;
6030
+ return {
6031
+ app: "brew-tui",
6032
+ version,
6033
+ platform: platform(),
6034
+ os: release(),
6035
+ arch: arch(),
6036
+ machineId,
6037
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
6038
+ level,
6039
+ message,
6040
+ stack,
6041
+ context
6042
+ };
6043
+ }
6044
+ async function reportError(err, context = {}) {
6045
+ const config = await resolveConfig();
6046
+ if (!config.enabled || !config.endpoint) return;
6047
+ const machineId = await getMachineId4();
6048
+ const version = true ? "0.6.0" : "unknown";
6049
+ await postReport(buildReport("error", err, context, machineId, version), config);
6050
+ }
6051
+ async function installCrashReporter() {
6052
+ if (_installed) return;
6053
+ const config = await resolveConfig();
6054
+ if (!config.enabled || !config.endpoint) return;
6055
+ _installed = true;
6056
+ const machineId = await getMachineId4();
6057
+ const version = true ? "0.6.0" : "unknown";
6058
+ process.on("uncaughtException", (err) => {
6059
+ void postReport(buildReport("fatal", err, { kind: "uncaughtException" }, machineId, version), config);
6060
+ });
6061
+ process.on("unhandledRejection", (reason) => {
6062
+ void postReport(buildReport("error", reason, { kind: "unhandledRejection" }, machineId, version), config);
6063
+ });
6064
+ logger.info("crash-reporter: enabled", { endpoint: config.endpoint });
6065
+ }
6066
+
5853
6067
  // src/index.tsx
5854
6068
  import { jsx as jsx36 } from "react/jsx-runtime";
5855
6069
  var [, , command, arg] = process.argv;
5856
6070
  async function runCli() {
5857
6071
  await ensureDataDirs();
6072
+ await installCrashReporter();
5858
6073
  if (command === "activate") {
5859
6074
  const key = arg?.trim() ?? "";
5860
6075
  if (!key) {
@@ -5966,7 +6181,7 @@ Snapshots: ${snapshots.length} (latest: ${latest ? formatDate(latest.capturedAt)
5966
6181
  } catch {
5967
6182
  }
5968
6183
  try {
5969
- const { loadSyncConfig: loadSyncConfig2 } = await import("./sync-engine-4ERSW4EQ.js");
6184
+ const { loadSyncConfig: loadSyncConfig2 } = await import("./sync-engine-CAFK4LHA.js");
5970
6185
  const syncConfig = await loadSyncConfig2();
5971
6186
  if (syncConfig?.lastSync) {
5972
6187
  console.log(`Sync: last sync ${formatDate(syncConfig.lastSync)}`);
@@ -5989,7 +6204,7 @@ Snapshots: ${snapshots.length} (latest: ${latest ? formatDate(latest.capturedAt)
5989
6204
  if (command === "install-brewbar") {
5990
6205
  await useLicenseStore.getState().initialize();
5991
6206
  const isPro = useLicenseStore.getState().isPro();
5992
- const { installBrewBar } = await import("./brewbar-installer-VVZRCP5K.js");
6207
+ const { installBrewBar } = await import("./brewbar-installer-BAHS6EEZ.js");
5993
6208
  try {
5994
6209
  await installBrewBar(isPro, arg === "--force");
5995
6210
  console.log(t("cli_brewbarInstalled"));
@@ -6000,7 +6215,7 @@ Snapshots: ${snapshots.length} (latest: ${latest ? formatDate(latest.capturedAt)
6000
6215
  return;
6001
6216
  }
6002
6217
  if (command === "uninstall-brewbar") {
6003
- const { uninstallBrewBar } = await import("./brewbar-installer-VVZRCP5K.js");
6218
+ const { uninstallBrewBar } = await import("./brewbar-installer-BAHS6EEZ.js");
6004
6219
  try {
6005
6220
  await uninstallBrewBar();
6006
6221
  console.log(t("cli_brewbarUninstalled"));
@@ -6031,7 +6246,7 @@ async function ensureBrewBarRunning() {
6031
6246
  if (process.platform !== "darwin") return;
6032
6247
  await useLicenseStore.getState().initialize();
6033
6248
  if (!useLicenseStore.getState().isPro()) return;
6034
- const { isBrewBarInstalled, installBrewBar, launchBrewBar } = await import("./brewbar-installer-VVZRCP5K.js");
6249
+ const { isBrewBarInstalled, installBrewBar, launchBrewBar } = await import("./brewbar-installer-BAHS6EEZ.js");
6035
6250
  try {
6036
6251
  if (!await isBrewBarInstalled()) {
6037
6252
  console.log(t("cli_brewbarInstalling"));
@@ -6045,6 +6260,6 @@ async function ensureBrewBarRunning() {
6045
6260
  }
6046
6261
  runCli().catch((err) => {
6047
6262
  console.error(err instanceof Error ? err.message : String(err));
6048
- process.exit(1);
6263
+ void reportError(err, { kind: "runCli-rejection" }).finally(() => process.exit(1));
6049
6264
  });
6050
6265
  //# sourceMappingURL=index.js.map