brew-tui 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,65 +1,70 @@
1
1
  # Brew-TUI
2
2
 
3
- A visual terminal UI for [Homebrew](https://brew.sh) package management.
3
+ **Manage Homebrew visually from your terminal and macOS menu bar.**
4
4
 
5
- ![License](https://img.shields.io/badge/license-MIT-blue)
6
- ![Node](https://img.shields.io/badge/node-%3E%3D18-brightgreen)
7
- ![npm](https://img.shields.io/npm/v/brew-tui)
5
+ [![npm](https://img.shields.io/npm/v/brew-tui)](https://www.npmjs.com/package/brew-tui)
6
+ [![Node](https://img.shields.io/badge/node-%3E%3D22-brightgreen)](https://nodejs.org/)
7
+ [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
8
+ [![Homebrew](https://img.shields.io/badge/homebrew-tap-orange)](https://github.com/MoLinesGitHub/homebrew-tap)
9
+ [![Tests](https://img.shields.io/badge/tests-99%20passing-brightgreen)]()
8
10
 
9
- ## Features
10
-
11
- - **Dashboard** -- overview of installed packages, outdated counts, services, and system info
12
- - **Installed** -- browse and filter formulae and casks with version info and status badges
13
- - **Search** -- find and install packages directly from the TUI
14
- - **Outdated** -- see available upgrades with version comparison arrows, upgrade individually or all at once
15
- - **Services** -- start, stop, and restart Homebrew services
16
- - **Doctor** -- run `brew doctor` and see warnings at a glance
17
- - **Package Info** -- detailed view with dependencies, caveats, and quick install/uninstall
11
+ > Two tools, one workflow: **Brew-TUI** is a keyboard-driven terminal UI, and **BrewBar** is a native macOS menu bar companion. Both call `brew` directly — no middleware, no daemons.
18
12
 
19
- ### Pro Features
13
+ **Website:** [molinesdesigns.com/brewtui](https://molinesdesigns.com/brewtui/)
20
14
 
21
- - **Profiles** -- export and import your Homebrew setup across machines
22
- - **Smart Cleanup** -- find orphaned packages and reclaim disk space
23
- - **Action History** -- track every install, uninstall, and upgrade
24
- - **Security Audit** -- scan packages against the OSV vulnerability database
15
+ ---
25
16
 
26
17
  ## Install
27
18
 
28
19
  ```bash
29
- # npm / pnpm / yarn / bun (all use the same npm registry)
20
+ # npm (recommended)
30
21
  npm install -g brew-tui
31
- pnpm add -g brew-tui
32
- yarn global add brew-tui
33
- bun add -g brew-tui
34
22
 
35
23
  # Homebrew
36
24
  brew tap MoLinesGitHub/tap
37
25
  brew install brew-tui
38
26
 
39
- # GitHub Packages
40
- npm install -g @MoLinesGitHub/brew-tui --registry https://npm.pkg.github.com
41
-
42
- # npx (run without installing)
27
+ # Run without installing
43
28
  npx brew-tui
44
29
  ```
45
30
 
46
- ## Usage
31
+ **Requirements:** Node.js >= 22, Homebrew, macOS
47
32
 
48
- ```bash
49
- brew-tui # Launch the TUI
50
- brew-tui status # Show license status
51
- brew-tui activate <key> # Activate Pro license
52
- brew-tui deactivate # Deactivate Pro license
53
- ```
33
+ ---
34
+
35
+ ## Features
36
+
37
+ | Feature | Description |
38
+ |---------|-------------|
39
+ | **Dashboard** | Overview of installed packages, outdated counts, services, and system info |
40
+ | **Installed** | Browse and filter formulae and casks with version info and status badges |
41
+ | **Search** | Find and install packages directly from the TUI |
42
+ | **Outdated** | Version comparison arrows, upgrade individually or all at once |
43
+ | **Services** | Start, stop, and restart Homebrew services |
44
+ | **Doctor** | Run `brew doctor` and see warnings at a glance |
45
+ | **Package Info** | Detailed view with dependencies, caveats, and quick actions |
54
46
 
55
- ### Install BrewBar (Pro)
47
+ ### Pro Features
48
+
49
+ | Feature | Description |
50
+ |---------|-------------|
51
+ | **Profiles** | Export and import your Homebrew setup across machines |
52
+ | **Smart Cleanup** | Find orphaned packages and reclaim disk space |
53
+ | **Action History** | Track every install, uninstall, and upgrade |
54
+ | **Security Audit** | Scan packages against the [OSV](https://osv.dev) vulnerability database |
55
+ | **BrewBar** | Native macOS menu bar app with notifications and one-click upgrades |
56
56
 
57
- BrewBar is a companion macOS menu bar app that shows outdated package counts, sends notifications, and lets you upgrade packages without opening a terminal. Pro users can install it directly from the CLI:
57
+ ---
58
+
59
+ ## Usage
58
60
 
59
61
  ```bash
60
- brew-tui install-brewbar # Download & install to /Applications
61
- brew-tui install-brewbar --force # Reinstall / update
62
- brew-tui uninstall-brewbar # Remove from /Applications
62
+ brew-tui # Launch the TUI
63
+ brew-tui status # Show license status
64
+ brew-tui activate <key> # Activate Pro license
65
+ brew-tui revalidate # Revalidate Pro license
66
+ brew-tui deactivate # Deactivate license on this machine
67
+ brew-tui delete-account # Remove all local data (~/.brew-tui/)
63
68
  ```
64
69
 
65
70
  ### Keyboard Navigation
@@ -75,18 +80,39 @@ brew-tui uninstall-brewbar # Remove from /Applications
75
80
  | `L` | Toggle language (en/es) |
76
81
  | `q` | Quit |
77
82
 
78
- ## Language
83
+ ### Language
79
84
 
80
- Brew-TUI supports English and Spanish. The language is detected automatically from your system locale (`LANG` environment variable). You can also:
85
+ Brew-TUI supports **English** and **Spanish**. Language is detected from your system locale (`LANG`), or you can:
81
86
 
82
87
  - Pass `--lang=es` or `--lang=en` as a CLI flag
83
- - Press `L` inside the TUI to toggle between languages
88
+ - Press `L` inside the TUI to toggle
89
+
90
+ ---
84
91
 
85
- ## BrewBar
92
+ ## BrewBar (Pro)
93
+
94
+ BrewBar is a native macOS menu bar companion app (Swift 6 / SwiftUI) that:
95
+
96
+ - Shows a badge with outdated package count
97
+ - Sends push notifications when updates are available
98
+ - Lets you upgrade packages without opening a terminal
99
+ - Displays Homebrew service status
100
+ - Configurable check interval (1h / 4h / 8h)
101
+ - Supports Launch at Login
102
+
103
+ ### Install BrewBar
104
+
105
+ ```bash
106
+ # Via Brew-TUI CLI (Pro license required)
107
+ brew-tui install-brewbar
108
+ brew-tui install-brewbar --force # Reinstall / update
109
+ brew-tui uninstall-brewbar # Remove
86
110
 
87
- BrewBar is a companion macOS menu bar app (Swift 6 / SwiftUI) that shows outdated package counts, sends notifications, and lets you upgrade packages without opening a terminal.
111
+ # Via Homebrew Cask
112
+ brew install --cask MoLinesGitHub/tap/brewbar
113
+ ```
88
114
 
89
- BrewBar lives in the `menubar/` directory and is built separately with [Tuist](https://tuist.io):
115
+ ### Build from Source
90
116
 
91
117
  ```bash
92
118
  cd menubar
@@ -94,14 +120,79 @@ tuist generate
94
120
  xcodebuild -workspace BrewBar.xcworkspace -scheme BrewBar build
95
121
  ```
96
122
 
97
- ## Requirements
123
+ Requires [Tuist](https://tuist.io), Xcode, and macOS 14+.
124
+
125
+ ---
126
+
127
+ ## Architecture
128
+
129
+ ```
130
+ Views (React/Ink) --> Stores (Zustand) --> brew-api --> Parsers --> brew CLI (spawn)
131
+ ```
132
+
133
+ | Layer | Tech | Role |
134
+ |-------|------|------|
135
+ | **UI** | React 18 + Ink 5 | Terminal rendering via ANSI escape codes |
136
+ | **State** | Zustand 5 | Global stores with per-key loading/error maps |
137
+ | **API** | brew-api.ts | Typed wrapper over `brew` CLI with input validation |
138
+ | **Parsers** | json-parser / text-parser | Parse `brew info --json`, `brew search`, `brew doctor` |
139
+ | **CLI** | brew-cli.ts | `execBrew()` (30s timeout) and `streamBrew()` (async generator, 5min idle timeout) |
140
+
141
+ - ESM-only, TypeScript strict mode, built with [tsup](https://github.com/egoist/tsup)
142
+ - All streaming operations (install, upgrade) use AsyncGenerators yielding lines in real time
143
+ - Package names validated via regex before passing to `spawn` (no shell injection)
144
+ - 99 tests across 10 suites (Vitest)
145
+
146
+ ---
147
+
148
+ ## Security
149
+
150
+ - License data encrypted with AES-256-GCM, machine-bound via UUID
151
+ - SHA-256 verification on BrewBar binary downloads
152
+ - Bundle integrity check at startup (fail-closed)
153
+ - Runtime validation of all external API responses (Polar, OSV)
154
+ - Rate limiting on license activation (5 attempts / 15min lockout)
155
+ - No secrets in logs, no PII transmitted without consent
156
+
157
+ ---
158
+
159
+ ## Project Structure
160
+
161
+ ```
162
+ src/
163
+ views/ # 12 React/Ink views
164
+ stores/ # Zustand stores (brew, navigation, license, modal)
165
+ components/ # Shared UI (StatusBadge, ResultBanner, SelectableRow, ...)
166
+ hooks/ # useKeyboard, useBrewStream, useDebounce
167
+ lib/
168
+ license/ # Polar API, AES encryption, anti-tamper, canary
169
+ security/ # OSV vulnerability scanning
170
+ profiles/ # Profile export/import (Pro)
171
+ cleanup/ # Orphan detection (Pro)
172
+ history/ # Action logging (Pro)
173
+ parsers/ # JSON and text parsers for brew output
174
+ i18n/ # English + Spanish translations
175
+ utils/ # Colors, spacing, logger, formatting
176
+ menubar/ # BrewBar (Swift 6 / SwiftUI / Tuist)
177
+ ```
178
+
179
+ ---
180
+
181
+ ## Contributing
98
182
 
99
- - **Node.js** >= 18
100
- - **Homebrew** installed on your system
101
- - **macOS** 14+ (for BrewBar)
183
+ ```bash
184
+ git clone https://github.com/MoLinesGitHub/Brew-TUI.git
185
+ cd Brew-TUI
186
+ npm install
187
+ npm run dev # Run with tsx (requires interactive TTY)
188
+ npm run typecheck # tsc --noEmit
189
+ npm run test # vitest (99 tests)
190
+ npm run lint # eslint
191
+ npm run build # Production bundle via tsup
192
+ ```
102
193
 
103
- > **Note:** Brew-TUI is designed for dark terminal backgrounds. Light terminal themes may have reduced visibility.
194
+ ---
104
195
 
105
196
  ## License
106
197
 
107
- [MIT](LICENSE) -- MoLines Designs
198
+ [MIT](LICENSE) -- [MoLines Designs](https://molinesdesigns.com)
@@ -1,21 +1,21 @@
1
1
  import {
2
2
  fetchWithTimeout,
3
- t,
4
- useLicenseStore,
5
- verifyPro
6
- } from "./chunk-KXDTKY3E.js";
3
+ t
4
+ } from "./chunk-PTLSNG2N.js";
7
5
 
8
6
  // src/lib/brewbar-installer.ts
9
7
  import { rm, access, readFile } from "fs/promises";
10
8
  import { createWriteStream } from "fs";
11
- import { createHash } from "crypto";
9
+ import { createHash, randomUUID } from "crypto";
10
+ import { tmpdir } from "os";
11
+ import { join } from "path";
12
12
  import { pipeline } from "stream/promises";
13
13
  import { execFile } from "child_process";
14
14
  import { promisify } from "util";
15
15
  var execFileAsync = promisify(execFile);
16
16
  var BREWBAR_APP_PATH = "/Applications/BrewBar.app";
17
17
  var DOWNLOAD_URL = "https://github.com/MoLinesGitHub/Brew-TUI/releases/latest/download/BrewBar.app.zip";
18
- var TMP_ZIP = "/tmp/BrewBar.app.zip";
18
+ var MAX_SIZE = 200 * 1024 * 1024;
19
19
  async function isBrewBarInstalled() {
20
20
  try {
21
21
  await access(BREWBAR_APP_PATH);
@@ -24,42 +24,73 @@ async function isBrewBarInstalled() {
24
24
  return false;
25
25
  }
26
26
  }
27
- async function installBrewBar(force = false) {
27
+ async function installBrewBar(isPro, force = false) {
28
28
  if (process.platform !== "darwin") {
29
29
  throw new Error(t("cli_brewbarMacOnly"));
30
30
  }
31
- const { license, status } = useLicenseStore.getState();
32
- if (!verifyPro(license, status)) {
31
+ if (!isPro) {
33
32
  throw new Error(t("cli_brewbarProRequired"));
34
33
  }
35
34
  if (!force && await isBrewBarInstalled()) {
36
35
  throw new Error(t("cli_brewbarAlreadyInstalled"));
37
36
  }
38
37
  console.log(t("cli_brewbarInstalling"));
38
+ const TMP_ZIP = join(tmpdir(), "BrewBar-" + randomUUID() + ".zip");
39
39
  const res = await fetchWithTimeout(DOWNLOAD_URL, {}, 12e4);
40
40
  if (!res.ok || !res.body) {
41
41
  throw new Error(t("cli_brewbarDownloadFailed", { error: `HTTP ${res.status}` }));
42
42
  }
43
43
  const contentLength = Number(res.headers.get("content-length") ?? "0");
44
- if (contentLength > 200 * 1024 * 1024) {
44
+ if (contentLength > MAX_SIZE) {
45
45
  throw new Error(t("cli_brewbarDownloadFailed", { error: "Download exceeds 200 MB size limit" }));
46
46
  }
47
+ let downloadedBytes = 0;
47
48
  const fileStream = createWriteStream(TMP_ZIP);
48
- await pipeline(res.body, fileStream);
49
+ const transformedBody = new ReadableStream({
50
+ async start(controller) {
51
+ const bodyReader = res.body.getReader();
52
+ try {
53
+ while (true) {
54
+ const { done, value } = await bodyReader.read();
55
+ if (done) break;
56
+ downloadedBytes += value.length;
57
+ if (downloadedBytes > MAX_SIZE) {
58
+ controller.error(new Error("Download exceeds 200 MB limit"));
59
+ return;
60
+ }
61
+ controller.enqueue(value);
62
+ }
63
+ controller.close();
64
+ } catch (err) {
65
+ controller.error(err);
66
+ }
67
+ }
68
+ });
69
+ await pipeline(transformedBody, fileStream);
70
+ let expectedHash = null;
49
71
  try {
50
72
  const checksumRes = await fetchWithTimeout(`${DOWNLOAD_URL}.sha256`, {}, 15e3);
51
73
  if (checksumRes.ok) {
52
- const expected = (await checksumRes.text()).trim().split(/\s+/)[0].toLowerCase();
53
- const fileBuffer = await readFile(TMP_ZIP);
54
- const actual = createHash("sha256").update(fileBuffer).digest("hex");
55
- if (actual !== expected) {
56
- await rm(TMP_ZIP, { force: true }).catch(() => {
57
- });
58
- throw new Error(t("cli_brewbarDownloadFailed", { error: "SHA-256 checksum mismatch" }));
74
+ const text = await checksumRes.text();
75
+ const hash = text.trim().split(/\s+/)[0];
76
+ if (hash && /^[0-9a-f]{64}$/i.test(hash)) {
77
+ expectedHash = hash.toLowerCase();
59
78
  }
60
79
  }
61
- } catch (err) {
62
- if (err instanceof Error && err.message.includes("checksum mismatch")) throw err;
80
+ } catch {
81
+ }
82
+ if (expectedHash) {
83
+ const fileBuffer = await readFile(TMP_ZIP);
84
+ const actual = createHash("sha256").update(fileBuffer).digest("hex");
85
+ if (actual !== expectedHash) {
86
+ await rm(TMP_ZIP, { force: true }).catch(() => {
87
+ });
88
+ throw new Error(t("cli_brewbarDownloadFailed", { error: "SHA-256 mismatch: binary may have been tampered with" }));
89
+ }
90
+ } else {
91
+ await rm(TMP_ZIP, { force: true }).catch(() => {
92
+ });
93
+ throw new Error(t("cli_brewbarDownloadFailed", { error: "SHA-256 checksum unavailable \u2014 cannot verify download integrity" }));
63
94
  }
64
95
  if (force && await isBrewBarInstalled()) {
65
96
  await rm(BREWBAR_APP_PATH, { recursive: true, force: true });
@@ -84,3 +115,4 @@ export {
84
115
  isBrewBarInstalled,
85
116
  uninstallBrewBar
86
117
  };
118
+ //# sourceMappingURL=brewbar-installer-H5MLNNTD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/brewbar-installer.ts"],"sourcesContent":["import { rm, access, readFile } from 'node:fs/promises';\nimport { createWriteStream } from 'node:fs';\nimport { createHash, randomUUID } from 'node:crypto';\nimport { tmpdir } from 'node:os';\nimport { join } from 'node:path';\nimport { pipeline } from 'node:stream/promises';\nimport { execFile } from 'node:child_process';\nimport { promisify } from 'node:util';\nimport { t } from '../i18n/index.js';\nimport { fetchWithTimeout } from './fetch-timeout.js';\n\nconst execFileAsync = promisify(execFile);\nconst BREWBAR_APP_PATH = '/Applications/BrewBar.app';\nconst DOWNLOAD_URL = 'https://github.com/MoLinesGitHub/Brew-TUI/releases/latest/download/BrewBar.app.zip';\nconst MAX_SIZE = 200 * 1024 * 1024; // 200 MB\n\nexport async function isBrewBarInstalled(): Promise<boolean> {\n try {\n await access(BREWBAR_APP_PATH);\n return true;\n } catch {\n return false;\n }\n}\n\nexport async function installBrewBar(isPro: boolean, force = false): Promise<void> {\n // macOS only\n if (process.platform !== 'darwin') {\n throw new Error(t('cli_brewbarMacOnly'));\n }\n\n // Pro check\n if (!isPro) {\n throw new Error(t('cli_brewbarProRequired'));\n }\n\n // Already installed check\n if (!force && await isBrewBarInstalled()) {\n throw new Error(t('cli_brewbarAlreadyInstalled'));\n }\n\n console.log(t('cli_brewbarInstalling'));\n\n // EP-013: Use unique temp path\n const TMP_ZIP = join(tmpdir(), 'BrewBar-' + randomUUID() + '.zip');\n\n // Download zip (120s timeout for large binary)\n const res = await fetchWithTimeout(DOWNLOAD_URL, {}, 120_000);\n if (!res.ok || !res.body) {\n throw new Error(t('cli_brewbarDownloadFailed', { error: `HTTP ${res.status}` }));\n }\n\n // Reject downloads larger than 200 MB (from Content-Length header)\n const contentLength = Number(res.headers.get('content-length') ?? '0');\n if (contentLength > MAX_SIZE) {\n throw new Error(t('cli_brewbarDownloadFailed', { error: 'Download exceeds 200 MB size limit' }));\n }\n\n // EP-005: Track downloaded bytes during the stream\n let downloadedBytes = 0;\n\n // Write to tmp file with byte counting\n const fileStream = createWriteStream(TMP_ZIP);\n const transformedBody = new ReadableStream({\n async start(controller) {\n const bodyReader = (res.body as ReadableStream<Uint8Array>).getReader();\n try {\n while (true) {\n const { done, value } = await bodyReader.read();\n if (done) break;\n downloadedBytes += value.length;\n if (downloadedBytes > MAX_SIZE) {\n controller.error(new Error('Download exceeds 200 MB limit'));\n return;\n }\n controller.enqueue(value);\n }\n controller.close();\n } catch (err) {\n controller.error(err);\n }\n },\n });\n await pipeline(transformedBody as unknown as NodeJS.ReadableStream, fileStream);\n\n // SEG-001: SHA-256 integrity check with proper error handling\n let expectedHash: string | null = null;\n try {\n const checksumRes = await fetchWithTimeout(`${DOWNLOAD_URL}.sha256`, {}, 15_000);\n if (checksumRes.ok) {\n const text = await checksumRes.text();\n // EP-009: Validate split result is defined\n const hash = text.trim().split(/\\s+/)[0];\n // EP-010: Validate hash format\n if (hash && /^[0-9a-f]{64}$/i.test(hash)) {\n expectedHash = hash.toLowerCase();\n }\n }\n } catch {\n /* checksum file not available */\n }\n\n if (expectedHash) {\n const fileBuffer = await readFile(TMP_ZIP);\n const actual = createHash('sha256').update(fileBuffer).digest('hex');\n if (actual !== expectedHash) {\n await rm(TMP_ZIP, { force: true }).catch(() => {});\n throw new Error(t('cli_brewbarDownloadFailed', { error: 'SHA-256 mismatch: binary may have been tampered with' }));\n }\n } else {\n // NUEVO-003: Treat missing checksum as fatal — don't install unverified binaries\n await rm(TMP_ZIP, { force: true }).catch(() => {});\n throw new Error(t('cli_brewbarDownloadFailed', { error: 'SHA-256 checksum unavailable — cannot verify download integrity' }));\n }\n\n // Remove old app if force reinstall\n if (force && await isBrewBarInstalled()) {\n await rm(BREWBAR_APP_PATH, { recursive: true, force: true });\n }\n\n // Unzip to /Applications\n try {\n await execFileAsync('ditto', ['-xk', TMP_ZIP, '/Applications/']);\n } catch (err) {\n throw new Error(t('cli_brewbarDownloadFailed', { error: err instanceof Error ? err.message : String(err) }));\n } finally {\n // Clean up tmp zip\n await rm(TMP_ZIP, { force: true }).catch(() => {});\n }\n}\n\nexport async function uninstallBrewBar(): Promise<void> {\n if (!await isBrewBarInstalled()) {\n throw new Error(t('cli_brewbarNotInstalled'));\n }\n\n await rm(BREWBAR_APP_PATH, { recursive: true, force: true });\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,103 @@
1
+ // src/lib/history/history-logger.ts
2
+ import { readFile, writeFile, rename, open, unlink } from "fs/promises";
3
+ import { randomUUID } from "crypto";
4
+
5
+ // src/lib/data-dir.ts
6
+ import { homedir } from "os";
7
+ import { join } from "path";
8
+ import { mkdir } from "fs/promises";
9
+ var DATA_DIR = join(homedir(), ".brew-tui");
10
+ var PROFILES_DIR = join(DATA_DIR, "profiles");
11
+ var LICENSE_PATH = join(DATA_DIR, "license.json");
12
+ var HISTORY_PATH = join(DATA_DIR, "history.json");
13
+ async function ensureDataDirs() {
14
+ await mkdir(DATA_DIR, { recursive: true, mode: 448 });
15
+ await mkdir(PROFILES_DIR, { recursive: true, mode: 448 });
16
+ }
17
+
18
+ // src/lib/history/history-logger.ts
19
+ var MAX_ENTRIES = 1e3;
20
+ function assertPro(isPro) {
21
+ if (!isPro) throw new Error("Pro license required");
22
+ }
23
+ var lockPath = HISTORY_PATH + ".lock";
24
+ async function withLock(fn) {
25
+ const lockFd = await open(lockPath, "wx").catch(() => null);
26
+ if (!lockFd) throw new Error("History file is locked by another process");
27
+ try {
28
+ return await fn();
29
+ } finally {
30
+ await lockFd.close();
31
+ await unlink(lockPath).catch(() => {
32
+ });
33
+ }
34
+ }
35
+ function detectAction(args) {
36
+ const cmd = args[0];
37
+ if (cmd === "install") return { action: "install", packageName: args[1] ?? null };
38
+ if (cmd === "uninstall") {
39
+ const name = args.find((a) => !a.startsWith("-")) === "uninstall" ? args.find((a, i) => i > 0 && !a.startsWith("-")) ?? null : args[1] ?? null;
40
+ return { action: "uninstall", packageName: name };
41
+ }
42
+ if (cmd === "upgrade") {
43
+ if (args.length === 1) return { action: "upgrade-all", packageName: null };
44
+ return { action: "upgrade", packageName: args[1] ?? null };
45
+ }
46
+ return null;
47
+ }
48
+ async function loadHistory(isPro) {
49
+ assertPro(isPro);
50
+ try {
51
+ const raw = await readFile(HISTORY_PATH, "utf-8");
52
+ const file = JSON.parse(raw);
53
+ if (file.version !== 1) {
54
+ throw new Error("Unsupported data version");
55
+ }
56
+ const entries = file.entries;
57
+ return Array.isArray(entries) ? entries : [];
58
+ } catch {
59
+ return [];
60
+ }
61
+ }
62
+ async function saveHistory(entries) {
63
+ await ensureDataDirs();
64
+ const file = { version: 1, entries };
65
+ const tmp = HISTORY_PATH + ".tmp";
66
+ await writeFile(tmp, JSON.stringify(file, null, 2), { encoding: "utf-8", mode: 384 });
67
+ await rename(tmp, HISTORY_PATH);
68
+ }
69
+ async function appendEntry(isPro, action, packageName, success, error = null) {
70
+ assertPro(isPro);
71
+ await withLock(async () => {
72
+ const entries = await loadHistory(isPro);
73
+ const entry = {
74
+ id: randomUUID(),
75
+ action,
76
+ packageName,
77
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
78
+ success,
79
+ error
80
+ };
81
+ entries.unshift(entry);
82
+ if (entries.length > MAX_ENTRIES) {
83
+ entries.length = MAX_ENTRIES;
84
+ }
85
+ await saveHistory(entries);
86
+ });
87
+ }
88
+ async function clearHistory(isPro) {
89
+ assertPro(isPro);
90
+ await saveHistory([]);
91
+ }
92
+
93
+ export {
94
+ DATA_DIR,
95
+ PROFILES_DIR,
96
+ LICENSE_PATH,
97
+ ensureDataDirs,
98
+ detectAction,
99
+ loadHistory,
100
+ appendEntry,
101
+ clearHistory
102
+ };
103
+ //# sourceMappingURL=chunk-65YZJX2E.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/history/history-logger.ts","../src/lib/data-dir.ts"],"sourcesContent":["import { readFile, writeFile, rename, open, unlink } from 'node:fs/promises';\nimport { randomUUID } from 'node:crypto';\nimport { HISTORY_PATH, ensureDataDirs } from '../data-dir.js';\nimport type { HistoryEntry, HistoryFile, HistoryAction } from './types.js';\n\nconst MAX_ENTRIES = 1000;\n\nfunction assertPro(isPro: boolean): void {\n if (!isPro) throw new Error('Pro license required');\n}\n\n// ── BK-004: Simple file locking ──\nconst lockPath = HISTORY_PATH + '.lock';\n\nasync function withLock<T>(fn: () => Promise<T>): Promise<T> {\n const lockFd = await open(lockPath, 'wx').catch(() => null);\n if (!lockFd) throw new Error('History file is locked by another process');\n try {\n return await fn();\n } finally {\n await lockFd.close();\n await unlink(lockPath).catch(() => {});\n }\n}\n\n/** Map brew subcommand to a history action type */\nexport function detectAction(args: string[]): { action: HistoryAction; packageName: string | null } | null {\n const cmd = args[0];\n if (cmd === 'install') return { action: 'install', packageName: args[1] ?? null };\n if (cmd === 'uninstall') {\n const name = args.find((a) => !a.startsWith('-')) === 'uninstall'\n ? args.find((a, i) => i > 0 && !a.startsWith('-')) ?? null\n : args[1] ?? null;\n return { action: 'uninstall', packageName: name };\n }\n if (cmd === 'upgrade') {\n if (args.length === 1) return { action: 'upgrade-all', packageName: null };\n return { action: 'upgrade', packageName: args[1] ?? null };\n }\n return null;\n}\n\nexport async function loadHistory(isPro: boolean): Promise<HistoryEntry[]> {\n assertPro(isPro);\n\n try {\n const raw = await readFile(HISTORY_PATH, 'utf-8');\n const file = JSON.parse(raw) as HistoryFile;\n if (file.version !== 1) {\n // Future: add migration logic here\n throw new Error('Unsupported data version');\n }\n const entries = file.entries;\n return Array.isArray(entries) ? entries : [];\n } catch {\n return [];\n }\n}\n\nasync function saveHistory(entries: HistoryEntry[]): Promise<void> {\n await ensureDataDirs();\n const file: HistoryFile = { version: 1, entries };\n const tmp = HISTORY_PATH + '.tmp';\n await writeFile(tmp, JSON.stringify(file, null, 2), { encoding: 'utf-8', mode: 0o600 });\n await rename(tmp, HISTORY_PATH);\n}\n\nexport async function appendEntry(\n isPro: boolean,\n action: HistoryAction,\n packageName: string | null,\n success: boolean,\n error: string | null = null,\n): Promise<void> {\n assertPro(isPro);\n\n await withLock(async () => {\n const entries = await loadHistory(isPro);\n\n const entry: HistoryEntry = {\n id: randomUUID(),\n action,\n packageName,\n timestamp: new Date().toISOString(),\n success,\n error,\n };\n\n entries.unshift(entry);\n\n if (entries.length > MAX_ENTRIES) {\n entries.length = MAX_ENTRIES;\n }\n\n await saveHistory(entries);\n });\n}\n\nexport async function clearHistory(isPro: boolean): Promise<void> {\n assertPro(isPro);\n await saveHistory([]);\n}\n","import { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { mkdir } from 'node:fs/promises';\n\nexport const DATA_DIR = join(homedir(), '.brew-tui');\nexport const PROFILES_DIR = join(DATA_DIR, 'profiles');\nexport const LICENSE_PATH = join(DATA_DIR, 'license.json');\nexport const HISTORY_PATH = join(DATA_DIR, 'history.json');\n\nexport async function ensureDataDirs(): Promise<void> {\n await mkdir(DATA_DIR, { recursive: true, mode: 0o700 });\n await mkdir(PROFILES_DIR, { recursive: true, mode: 0o700 });\n}\n"],"mappings":"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","names":[]}