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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Brew-TUI
2
2
 
3
- **Manage Homebrew visually from your terminal and macOS menu bar.**
3
+ ### Your Homebrew, finally visible.
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/brew-tui)](https://www.npmjs.com/package/brew-tui)
6
6
  [![Node](https://img.shields.io/badge/node-%3E%3D22-brightgreen)](https://nodejs.org/)
@@ -8,27 +8,45 @@
8
8
  [![Homebrew](https://img.shields.io/badge/homebrew-tap-orange)](https://github.com/MoLinesGitHub/homebrew-tap)
9
9
  [![Tests](https://img.shields.io/badge/tests-99%20passing-brightgreen)]()
10
10
 
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.
11
+ A keyboard-driven terminal UI for Homebrew, with a native macOS menu bar companion that watches updates in the background. No daemons, no middleware — both tools call `brew` directly.
12
12
 
13
- **Website:** [molinesdesigns.com/brewtui](https://molinesdesigns.com/brewtui/)
13
+ ![Brew-TUI demo](assets/demo.gif)
14
+
15
+ ```bash
16
+ brew tap MoLinesGitHub/tap
17
+ brew install brew-tui # then just type: brew-tui
18
+ ```
19
+
20
+ ---
21
+
22
+ ## Why Brew-TUI?
23
+
24
+ You don't memorize `brew outdated && brew upgrade && brew services list && brew leaves`. You forget half of them. Brew-TUI puts every command behind one keystroke and shows you what `brew` never tells you until something breaks: orphans, vulnerabilities, services that died last Tuesday.
25
+
26
+ | Without Brew-TUI | With Brew-TUI |
27
+ |---|---|
28
+ | `brew outdated` → wall of text → grep | Press **3** → list with version arrows → `Enter` to upgrade |
29
+ | `brew services list` → restart by hand | Press **4** → toggle services with one key |
30
+ | Vulnerable packages? | Press **9** → cross-checked against [OSV.dev](https://osv.dev) (Pro) |
31
+ | Forgot to update? | **BrewBar** lives in your menu bar and tells you (Pro) |
14
32
 
15
33
  ---
16
34
 
17
35
  ## Install
18
36
 
19
37
  ```bash
20
- # npm (recommended)
21
- npm install -g brew-tui
22
-
23
- # Homebrew
38
+ # Homebrew (recommended)
24
39
  brew tap MoLinesGitHub/tap
25
40
  brew install brew-tui
26
41
 
42
+ # npm
43
+ npm install -g brew-tui
44
+
27
45
  # Run without installing
28
46
  npx brew-tui
29
47
  ```
30
48
 
31
- **Requirements:** Node.js >= 22, Homebrew, macOS
49
+ **Requirements:** Homebrew, macOS. The Homebrew formula installs the required Node.js runtime dependency.
32
50
 
33
51
  ---
34
52
 
@@ -44,15 +62,31 @@ npx brew-tui
44
62
  | **Doctor** | Run `brew doctor` and see warnings at a glance |
45
63
  | **Package Info** | Detailed view with dependencies, caveats, and quick actions |
46
64
 
47
- ### Pro Features
65
+ ### Pro Features — $19 once, lifetime
48
66
 
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 |
67
+ | Feature | What it solves |
68
+ |---------|----------------|
69
+ | **Profiles** | Replicate your exact setup on a new Mac in one command |
70
+ | **Smart Cleanup** | Reclaim gigabytes by listing orphans ranked by size |
71
+ | **Action History** | "What did I install last week?" answered |
72
+ | **Security Audit** | Get notified when [OSV.dev](https://osv.dev) flags something you have installed |
73
+ | **BrewBar** | A menu bar app that watches your packages while you sleep — auto-installs and auto-launches the moment you go Pro |
74
+
75
+ > Pro is one-time payment, lifetime updates, machine-bound. No subscription. [Activate →](https://molinesdesigns.com/brewtui/pro)
76
+
77
+ ---
78
+
79
+ ## Screenshots
80
+
81
+ | Dashboard | Outdated |
82
+ |---|---|
83
+ | ![Dashboard](assets/screenshots/dashboard.png) | ![Outdated](assets/screenshots/outdated.png) |
84
+ | **Services** | **Doctor** |
85
+ | ![Services](assets/screenshots/services.png) | ![Doctor](assets/screenshots/doctor.png) |
86
+ | **Smart Cleanup (Pro)** | **Security Audit (Pro)** |
87
+ | ![Smart Cleanup](assets/screenshots/smart-cleanup.png) | ![Security Audit](assets/screenshots/security-audit.png) |
88
+
89
+ > Smart Cleanup ranks orphans by size — find your 34 MB unused `jpeg-xl` in 2 seconds. Security Audit cross-checks every installed package against [OSV.dev](https://osv.dev) live.
56
90
 
57
91
  ---
58
92
 
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  fetchWithTimeout,
3
3
  t
4
- } from "./chunk-E7ZREAJW.js";
4
+ } from "./chunk-KDJNCZD7.js";
5
+ import "./chunk-KDHEUNRI.js";
5
6
 
6
7
  // src/lib/brewbar-installer.ts
7
8
  import { rm, access, readFile } from "fs/promises";
@@ -98,7 +99,7 @@ async function installBrewBar(isPro, force = false) {
98
99
  try {
99
100
  await execFileAsync("ditto", ["-xk", TMP_ZIP, "/Applications/"]);
100
101
  } catch (err) {
101
- throw new Error(t("cli_brewbarDownloadFailed", { error: err instanceof Error ? err.message : String(err) }));
102
+ throw new Error(t("cli_brewbarDownloadFailed", { error: err instanceof Error ? err.message : String(err) }), { cause: err });
102
103
  } finally {
103
104
  await rm(TMP_ZIP, { force: true }).catch(() => {
104
105
  });
@@ -124,4 +125,4 @@ export {
124
125
  launchBrewBar,
125
126
  uninstallBrewBar
126
127
  };
127
- //# sourceMappingURL=brewbar-installer-6BR3MPVZ.js.map
128
+ //# sourceMappingURL=brewbar-installer-ZEMXNDHP.js.map
@@ -1 +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\n/// Launches BrewBar detached from the parent process so it survives terminal close.\n/// `open -g -a` runs the app in the background without bringing it to foreground.\nexport async function launchBrewBar(): Promise<void> {\n if (process.platform !== 'darwin') return;\n if (!await isBrewBarInstalled()) return;\n try {\n await execFileAsync('open', ['-g', '-a', BREWBAR_APP_PATH]);\n } catch {\n // Non-fatal: BrewBar may already be running, or LaunchServices may need a moment.\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":[]}
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) }), { cause: err });\n } finally {\n // Clean up tmp zip\n await rm(TMP_ZIP, { force: true }).catch(() => {});\n }\n}\n\n/// Launches BrewBar detached from the parent process so it survives terminal close.\n/// `open -g -a` runs the app in the background without bringing it to foreground.\nexport async function launchBrewBar(): Promise<void> {\n if (process.platform !== 'darwin') return;\n if (!await isBrewBarInstalled()) return;\n try {\n await execFileAsync('open', ['-g', '-a', BREWBAR_APP_PATH]);\n } catch {\n // Non-fatal: BrewBar may already be running, or LaunchServices may need a moment.\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,20 @@
1
+ import {
2
+ BREWFILE_PATH,
3
+ computeDrift,
4
+ createDefaultBrewfile,
5
+ loadBrewfile,
6
+ reconcile,
7
+ saveBrewfile
8
+ } from "./chunk-LXF72RCD.js";
9
+ import "./chunk-4I344KQX.js";
10
+ import "./chunk-UWS4A4F5.js";
11
+ import "./chunk-KDHEUNRI.js";
12
+ export {
13
+ BREWFILE_PATH,
14
+ computeDrift,
15
+ createDefaultBrewfile,
16
+ loadBrewfile,
17
+ reconcile,
18
+ saveBrewfile
19
+ };
20
+ //# sourceMappingURL=brewfile-manager-3SERRYNC.js.map
@@ -0,0 +1,299 @@
1
+ import {
2
+ captureSnapshot
3
+ } from "./chunk-4I344KQX.js";
4
+ import {
5
+ DATA_DIR
6
+ } from "./chunk-UWS4A4F5.js";
7
+ import {
8
+ logger
9
+ } from "./chunk-KDHEUNRI.js";
10
+
11
+ // src/lib/sync/sync-engine.ts
12
+ import { readFile as readFile2, writeFile as writeFile2, rename as rename2 } from "fs/promises";
13
+ import { join as join2 } from "path";
14
+ import { hostname } from "os";
15
+
16
+ // src/lib/sync/crypto.ts
17
+ import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "crypto";
18
+ var ENCRYPTION_SECRET = "brew-tui-sync-aes256gcm-v1";
19
+ var SCRYPT_SALT = "brew-tui-sync-salt-v1";
20
+ var _derivedKey = null;
21
+ function deriveEncryptionKey() {
22
+ if (!_derivedKey) {
23
+ _derivedKey = scryptSync(ENCRYPTION_SECRET, SCRYPT_SALT, 32, {
24
+ N: 16384,
25
+ r: 8,
26
+ p: 1
27
+ });
28
+ }
29
+ return _derivedKey;
30
+ }
31
+ function encryptPayload(data) {
32
+ const key = deriveEncryptionKey();
33
+ const iv = randomBytes(12);
34
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
35
+ const plaintext = JSON.stringify(data);
36
+ const ciphertext = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
37
+ const tag = cipher.getAuthTag();
38
+ return {
39
+ encrypted: ciphertext.toString("base64"),
40
+ iv: iv.toString("base64"),
41
+ tag: tag.toString("base64")
42
+ };
43
+ }
44
+ function decryptPayload(encrypted, iv, tag) {
45
+ const key = deriveEncryptionKey();
46
+ const decipher = createDecipheriv(
47
+ "aes-256-gcm",
48
+ key,
49
+ Buffer.from(iv, "base64")
50
+ );
51
+ decipher.setAuthTag(Buffer.from(tag, "base64"));
52
+ const plaintext = Buffer.concat([
53
+ decipher.update(Buffer.from(encrypted, "base64")),
54
+ decipher.final()
55
+ ]);
56
+ return JSON.parse(plaintext.toString("utf-8"));
57
+ }
58
+
59
+ // src/lib/sync/backends/icloud-backend.ts
60
+ import { readFile, writeFile, rename, mkdir, stat } from "fs/promises";
61
+ import { homedir } from "os";
62
+ import { join } from "path";
63
+ var ICLOUD_BASE = join(
64
+ homedir(),
65
+ "Library",
66
+ "Mobile Documents",
67
+ "com~apple~CloudDocs"
68
+ );
69
+ var ICLOUD_SYNC_DIR = join(ICLOUD_BASE, "BrewTUI");
70
+ var ICLOUD_SYNC_PATH = join(ICLOUD_SYNC_DIR, "sync.json");
71
+ async function isICloudAvailable() {
72
+ try {
73
+ await stat(ICLOUD_BASE);
74
+ return true;
75
+ } catch {
76
+ return false;
77
+ }
78
+ }
79
+ function isValidEnvelope(v) {
80
+ if (!v || typeof v !== "object") return false;
81
+ const obj = v;
82
+ return obj["schemaVersion"] === 1 && typeof obj["encrypted"] === "string" && typeof obj["iv"] === "string" && typeof obj["tag"] === "string" && typeof obj["updatedAt"] === "string";
83
+ }
84
+ async function readSyncEnvelope() {
85
+ try {
86
+ const raw = await readFile(ICLOUD_SYNC_PATH, "utf-8");
87
+ const parsed = JSON.parse(raw);
88
+ if (!isValidEnvelope(parsed)) {
89
+ logger.warn("sync: invalid envelope structure in iCloud file");
90
+ return null;
91
+ }
92
+ return parsed;
93
+ } catch (err) {
94
+ if (err instanceof Error && err.code === "ENOENT") {
95
+ return null;
96
+ }
97
+ logger.warn("sync: could not read iCloud envelope", { error: String(err) });
98
+ return null;
99
+ }
100
+ }
101
+ async function writeSyncEnvelope(envelope) {
102
+ await mkdir(ICLOUD_SYNC_DIR, { recursive: true });
103
+ const tmpPath = ICLOUD_SYNC_PATH + ".tmp";
104
+ await writeFile(tmpPath, JSON.stringify(envelope, null, 2), {
105
+ encoding: "utf-8",
106
+ mode: 420
107
+ });
108
+ await rename(tmpPath, ICLOUD_SYNC_PATH);
109
+ }
110
+
111
+ // src/lib/sync/sync-engine.ts
112
+ var SYNC_CONFIG_PATH = join2(DATA_DIR, "sync-config.json");
113
+ var MACHINE_ID_PATH = join2(DATA_DIR, "machine-id");
114
+ async function loadSyncConfig() {
115
+ try {
116
+ const raw = await readFile2(SYNC_CONFIG_PATH, "utf-8");
117
+ return JSON.parse(raw);
118
+ } catch {
119
+ return null;
120
+ }
121
+ }
122
+ async function saveSyncConfig(config) {
123
+ const tmpPath = SYNC_CONFIG_PATH + ".tmp";
124
+ await writeFile2(tmpPath, JSON.stringify(config, null, 2), {
125
+ encoding: "utf-8",
126
+ mode: 384
127
+ });
128
+ await rename2(tmpPath, SYNC_CONFIG_PATH);
129
+ }
130
+ async function getMachineId() {
131
+ try {
132
+ const id = (await readFile2(MACHINE_ID_PATH, "utf-8")).trim();
133
+ if (id) return id;
134
+ } catch {
135
+ }
136
+ return hostname();
137
+ }
138
+ function detectConflicts(localSnapshot, otherMachines, localMachineId) {
139
+ const conflicts = [];
140
+ const localFormulaMap = new Map(localSnapshot.formulae.map((f) => [f.name, f.version]));
141
+ const localCaskMap = new Map(localSnapshot.casks.map((c) => [c.name, c.version]));
142
+ for (const machine of otherMachines) {
143
+ if (machine.machineId === localMachineId) continue;
144
+ for (const remoteFormula of machine.snapshot.formulae) {
145
+ const localVersion = localFormulaMap.get(remoteFormula.name);
146
+ if (localVersion !== void 0 && localVersion !== remoteFormula.version) {
147
+ conflicts.push({
148
+ packageName: remoteFormula.name,
149
+ packageType: "formula",
150
+ localVersion,
151
+ remoteMachine: machine.machineName,
152
+ remoteVersion: remoteFormula.version
153
+ });
154
+ }
155
+ }
156
+ for (const remoteCask of machine.snapshot.casks) {
157
+ const localVersion = localCaskMap.get(remoteCask.name);
158
+ if (localVersion !== void 0 && localVersion !== remoteCask.version) {
159
+ conflicts.push({
160
+ packageName: remoteCask.name,
161
+ packageType: "cask",
162
+ localVersion,
163
+ remoteMachine: machine.machineName,
164
+ remoteVersion: remoteCask.version
165
+ });
166
+ }
167
+ }
168
+ }
169
+ return conflicts;
170
+ }
171
+ async function writeEnvelope(payload) {
172
+ const now = (/* @__PURE__ */ new Date()).toISOString();
173
+ const { encrypted, iv, tag } = encryptPayload(payload);
174
+ const envelope = {
175
+ schemaVersion: 1,
176
+ encrypted,
177
+ iv,
178
+ tag,
179
+ updatedAt: now
180
+ };
181
+ await writeSyncEnvelope(envelope);
182
+ return now;
183
+ }
184
+ function mergePayload(existing, localState) {
185
+ return {
186
+ machines: {
187
+ ...existing.machines,
188
+ [localState.machineId]: localState
189
+ }
190
+ };
191
+ }
192
+ async function sync(isPro, currentBrewfile) {
193
+ if (!isPro) {
194
+ throw new Error("Pro license required");
195
+ }
196
+ const available = await isICloudAvailable();
197
+ if (!available) {
198
+ return {
199
+ success: false,
200
+ conflicts: [],
201
+ resolvedCount: 0,
202
+ error: "iCloud Drive not available"
203
+ };
204
+ }
205
+ let existingPayload = null;
206
+ try {
207
+ const envelope = await readSyncEnvelope();
208
+ if (envelope) {
209
+ existingPayload = decryptPayload(envelope.encrypted, envelope.iv, envelope.tag);
210
+ }
211
+ } catch (err) {
212
+ logger.warn("sync: could not decrypt existing payload, starting fresh", { error: String(err) });
213
+ existingPayload = null;
214
+ }
215
+ const snapshot = await captureSnapshot();
216
+ const machineId = await getMachineId();
217
+ const machineName = hostname();
218
+ const localState = {
219
+ machineId,
220
+ machineName,
221
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
222
+ snapshot,
223
+ ...currentBrewfile ? { brewfile: currentBrewfile } : {}
224
+ };
225
+ const otherMachines = existingPayload ? Object.values(existingPayload.machines).filter((m) => m.machineId !== machineId) : [];
226
+ const conflicts = detectConflicts(snapshot, otherMachines, machineId);
227
+ const basePayload = existingPayload ?? { machines: {} };
228
+ const mergedPayload = mergePayload(basePayload, localState);
229
+ if (conflicts.length > 0) {
230
+ await writeEnvelope(mergedPayload);
231
+ return {
232
+ success: false,
233
+ conflicts,
234
+ resolvedCount: 0
235
+ };
236
+ }
237
+ const now = await writeEnvelope(mergedPayload);
238
+ const existingConfig = await loadSyncConfig();
239
+ await saveSyncConfig({
240
+ enabled: true,
241
+ machineId,
242
+ machineName,
243
+ ...existingConfig ?? {},
244
+ lastSync: now
245
+ });
246
+ logger.info("sync: completed successfully", { machineId, machines: Object.keys(mergedPayload.machines).length });
247
+ return {
248
+ success: true,
249
+ conflicts: [],
250
+ resolvedCount: 0
251
+ };
252
+ }
253
+ async function applyConflictResolutions(payload, resolutions, localMachineId) {
254
+ const updatedPayload = {
255
+ machines: { ...payload.machines }
256
+ };
257
+ for (const { conflict, resolution } of resolutions) {
258
+ if (resolution !== "use-remote") continue;
259
+ const localMachine = updatedPayload.machines[localMachineId];
260
+ if (!localMachine) {
261
+ logger.warn("sync: cannot apply resolution, local machine missing in payload", { localMachineId });
262
+ continue;
263
+ }
264
+ if (conflict.packageType === "formula") {
265
+ updatedPayload.machines[localMachineId] = {
266
+ ...localMachine,
267
+ snapshot: {
268
+ ...localMachine.snapshot,
269
+ formulae: localMachine.snapshot.formulae.map(
270
+ (f) => f.name === conflict.packageName ? { ...f, version: conflict.remoteVersion } : f
271
+ )
272
+ }
273
+ };
274
+ } else {
275
+ updatedPayload.machines[localMachineId] = {
276
+ ...localMachine,
277
+ snapshot: {
278
+ ...localMachine.snapshot,
279
+ casks: localMachine.snapshot.casks.map(
280
+ (c) => c.name === conflict.packageName ? { ...c, version: conflict.remoteVersion } : c
281
+ )
282
+ }
283
+ };
284
+ }
285
+ }
286
+ await writeEnvelope(updatedPayload);
287
+ logger.info("sync: conflict resolutions applied", { count: resolutions.length });
288
+ }
289
+
290
+ export {
291
+ decryptPayload,
292
+ readSyncEnvelope,
293
+ loadSyncConfig,
294
+ saveSyncConfig,
295
+ getMachineId,
296
+ sync,
297
+ applyConflictResolutions
298
+ };
299
+ //# sourceMappingURL=chunk-42URLVAJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/sync/sync-engine.ts","../src/lib/sync/crypto.ts","../src/lib/sync/backends/icloud-backend.ts"],"sourcesContent":["import { readFile, writeFile, rename } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { hostname } from 'node:os';\nimport { encryptPayload, decryptPayload } from './crypto.js';\nimport {\n readSyncEnvelope,\n writeSyncEnvelope,\n isICloudAvailable,\n} from './backends/icloud-backend.js';\nimport { captureSnapshot } from '../state-snapshot/snapshot.js';\nimport { DATA_DIR } from '../data-dir.js';\nimport { logger } from '../../utils/logger.js';\nimport type {\n SyncConfig,\n SyncPayload,\n SyncConflict,\n SyncResult,\n MachineState,\n SyncEnvelope,\n} from './types.js';\nimport type { BrewfileSchema } from '../brewfile/types.js';\n\nconst SYNC_CONFIG_PATH = join(DATA_DIR, 'sync-config.json');\nconst MACHINE_ID_PATH = join(DATA_DIR, 'machine-id');\n\n// ── Config I/O ──────────────────────────────────────────────────────────────\n\nexport async function loadSyncConfig(): Promise<SyncConfig | null> {\n try {\n const raw = await readFile(SYNC_CONFIG_PATH, 'utf-8');\n return JSON.parse(raw) as SyncConfig;\n } catch {\n return null;\n }\n}\n\nexport async function saveSyncConfig(config: SyncConfig): Promise<void> {\n const tmpPath = SYNC_CONFIG_PATH + '.tmp';\n await writeFile(tmpPath, JSON.stringify(config, null, 2), {\n encoding: 'utf-8',\n mode: 0o600,\n });\n await rename(tmpPath, SYNC_CONFIG_PATH);\n}\n\n// ── Machine ID ───────────────────────────────────────────────────────────────\n\nexport async function getMachineId(): Promise<string> {\n try {\n const id = (await readFile(MACHINE_ID_PATH, 'utf-8')).trim();\n if (id) return id;\n } catch { /* machine-id created by polar-api on first activation */ }\n return hostname(); // Fallback: hostname if machine-id not yet created\n}\n\n// ── Conflict detection ───────────────────────────────────────────────────────\n\nfunction detectConflicts(\n localSnapshot: { formulae: Array<{ name: string; version: string }>; casks: Array<{ name: string; version: string }> },\n otherMachines: MachineState[],\n localMachineId: string,\n): SyncConflict[] {\n const conflicts: SyncConflict[] = [];\n\n const localFormulaMap = new Map(localSnapshot.formulae.map((f) => [f.name, f.version]));\n const localCaskMap = new Map(localSnapshot.casks.map((c) => [c.name, c.version]));\n\n for (const machine of otherMachines) {\n if (machine.machineId === localMachineId) continue;\n\n // Check formula conflicts: same package, different version on both machines\n for (const remoteFormula of machine.snapshot.formulae) {\n const localVersion = localFormulaMap.get(remoteFormula.name);\n if (localVersion !== undefined && localVersion !== remoteFormula.version) {\n conflicts.push({\n packageName: remoteFormula.name,\n packageType: 'formula',\n localVersion,\n remoteMachine: machine.machineName,\n remoteVersion: remoteFormula.version,\n });\n }\n }\n\n // Check cask conflicts\n for (const remoteCask of machine.snapshot.casks) {\n const localVersion = localCaskMap.get(remoteCask.name);\n if (localVersion !== undefined && localVersion !== remoteCask.version) {\n conflicts.push({\n packageName: remoteCask.name,\n packageType: 'cask',\n localVersion,\n remoteMachine: machine.machineName,\n remoteVersion: remoteCask.version,\n });\n }\n }\n }\n\n return conflicts;\n}\n\n// ── Merge ────────────────────────────────────────────────────────────────────\n\nasync function writeEnvelope(payload: SyncPayload): Promise<string> {\n const now = new Date().toISOString();\n const { encrypted, iv, tag } = encryptPayload(payload);\n const envelope: SyncEnvelope = {\n schemaVersion: 1,\n encrypted,\n iv,\n tag,\n updatedAt: now,\n };\n await writeSyncEnvelope(envelope);\n return now;\n}\n\nfunction mergePayload(existing: SyncPayload, localState: MachineState): SyncPayload {\n return {\n machines: {\n ...existing.machines,\n [localState.machineId]: localState,\n },\n };\n}\n\n// ── Main sync function ───────────────────────────────────────────────────────\n\nexport async function sync(\n isPro: boolean,\n currentBrewfile?: BrewfileSchema,\n): Promise<SyncResult> {\n if (!isPro) {\n throw new Error('Pro license required');\n }\n\n const available = await isICloudAvailable();\n if (!available) {\n return {\n success: false,\n conflicts: [],\n resolvedCount: 0,\n error: 'iCloud Drive not available',\n };\n }\n\n let existingPayload: SyncPayload | null = null;\n\n try {\n const envelope = await readSyncEnvelope();\n if (envelope) {\n existingPayload = decryptPayload(envelope.encrypted, envelope.iv, envelope.tag);\n }\n } catch (err) {\n logger.warn('sync: could not decrypt existing payload, starting fresh', { error: String(err) });\n existingPayload = null;\n }\n\n // Capture current local state\n const snapshot = await captureSnapshot();\n const machineId = await getMachineId();\n const machineName = hostname();\n\n const localState: MachineState = {\n machineId,\n machineName,\n updatedAt: new Date().toISOString(),\n snapshot,\n ...(currentBrewfile ? { brewfile: currentBrewfile } : {}),\n };\n\n // Detect conflicts against other machines in the payload\n const otherMachines = existingPayload\n ? Object.values(existingPayload.machines).filter((m) => m.machineId !== machineId)\n : [];\n\n const conflicts = detectConflicts(snapshot, otherMachines, machineId);\n\n // Always write the local machine state to the payload, even when conflicts\n // exist, so that applyConflictResolutions() has a local entry to update.\n // Without this, the iCloud envelope keeps only remote machines, and\n // resolution updates are silently dropped (they require localMachine to exist).\n const basePayload: SyncPayload = existingPayload ?? { machines: {} };\n const mergedPayload = mergePayload(basePayload, localState);\n\n if (conflicts.length > 0) {\n // Persist local state, then surface conflicts so the user can resolve them.\n await writeEnvelope(mergedPayload);\n return {\n success: false,\n conflicts,\n resolvedCount: 0,\n };\n }\n\n const now = await writeEnvelope(mergedPayload);\n\n // Update local sync config\n const existingConfig = await loadSyncConfig();\n await saveSyncConfig({\n enabled: true,\n machineId,\n machineName,\n ...(existingConfig ?? {}),\n lastSync: now,\n });\n\n logger.info('sync: completed successfully', { machineId, machines: Object.keys(mergedPayload.machines).length });\n\n return {\n success: true,\n conflicts: [],\n resolvedCount: 0,\n };\n}\n\n// ── Conflict resolution ──────────────────────────────────────────────────────\n\nexport async function applyConflictResolutions(\n payload: SyncPayload,\n resolutions: Array<{ conflict: SyncConflict; resolution: 'use-local' | 'use-remote' }>,\n localMachineId: string,\n): Promise<void> {\n // Work on a mutable copy\n const updatedPayload: SyncPayload = {\n machines: { ...payload.machines },\n };\n\n for (const { conflict, resolution } of resolutions) {\n if (resolution !== 'use-remote') continue;\n // Re-read latest local machine on every iteration so consecutive resolutions\n // build on top of each other instead of overwriting prior changes.\n const localMachine = updatedPayload.machines[localMachineId];\n if (!localMachine) {\n logger.warn('sync: cannot apply resolution, local machine missing in payload', { localMachineId });\n continue;\n }\n if (conflict.packageType === 'formula') {\n updatedPayload.machines[localMachineId] = {\n ...localMachine,\n snapshot: {\n ...localMachine.snapshot,\n formulae: localMachine.snapshot.formulae.map((f) =>\n f.name === conflict.packageName\n ? { ...f, version: conflict.remoteVersion }\n : f,\n ),\n },\n };\n } else {\n updatedPayload.machines[localMachineId] = {\n ...localMachine,\n snapshot: {\n ...localMachine.snapshot,\n casks: localMachine.snapshot.casks.map((c) =>\n c.name === conflict.packageName\n ? { ...c, version: conflict.remoteVersion }\n : c,\n ),\n },\n };\n }\n }\n\n await writeEnvelope(updatedPayload);\n logger.info('sync: conflict resolutions applied', { count: resolutions.length });\n}\n","import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'node:crypto';\nimport type { SyncPayload } from './types.js';\n\n// Cross-machine sync encryption — shared secret (no machine binding by design,\n// the same user's machines must decrypt each other's payloads).\nconst ENCRYPTION_SECRET = 'brew-tui-sync-aes256gcm-v1';\nconst SCRYPT_SALT = 'brew-tui-sync-salt-v1';\n\n// Lazy derivation — scryptSync is CPU-intensive and should not block on import.\nlet _derivedKey: Buffer | null = null;\n\nfunction deriveEncryptionKey(): Buffer {\n if (!_derivedKey) {\n _derivedKey = scryptSync(ENCRYPTION_SECRET, SCRYPT_SALT, 32, {\n N: 16384,\n r: 8,\n p: 1,\n });\n }\n return _derivedKey;\n}\n\nexport function encryptPayload(data: SyncPayload): { encrypted: string; iv: string; tag: string } {\n const key = deriveEncryptionKey();\n const iv = randomBytes(12); // 96-bit IV for GCM\n const cipher = createCipheriv('aes-256-gcm', key, iv);\n\n const plaintext = JSON.stringify(data);\n const ciphertext = Buffer.concat([cipher.update(plaintext, 'utf-8'), cipher.final()]);\n const tag = cipher.getAuthTag();\n\n return {\n encrypted: ciphertext.toString('base64'),\n iv: iv.toString('base64'),\n tag: tag.toString('base64'),\n };\n}\n\nexport function decryptPayload(encrypted: string, iv: string, tag: string): SyncPayload {\n const key = deriveEncryptionKey();\n const decipher = createDecipheriv(\n 'aes-256-gcm',\n key,\n Buffer.from(iv, 'base64'),\n );\n decipher.setAuthTag(Buffer.from(tag, 'base64'));\n\n const plaintext = Buffer.concat([\n decipher.update(Buffer.from(encrypted, 'base64')),\n decipher.final(),\n ]);\n\n return JSON.parse(plaintext.toString('utf-8')) as SyncPayload;\n}\n","import { readFile, writeFile, rename, mkdir, stat } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { logger } from '../../../utils/logger.js';\nimport type { SyncEnvelope } from '../types.js';\n\nconst ICLOUD_BASE = join(\n homedir(),\n 'Library', 'Mobile Documents', 'com~apple~CloudDocs',\n);\nexport const ICLOUD_SYNC_DIR = join(ICLOUD_BASE, 'BrewTUI');\nexport const ICLOUD_SYNC_PATH = join(ICLOUD_SYNC_DIR, 'sync.json');\n\nexport async function isICloudAvailable(): Promise<boolean> {\n try {\n await stat(ICLOUD_BASE);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction isValidEnvelope(v: unknown): v is SyncEnvelope {\n if (!v || typeof v !== 'object') return false;\n const obj = v as Record<string, unknown>;\n return (\n obj['schemaVersion'] === 1 &&\n typeof obj['encrypted'] === 'string' &&\n typeof obj['iv'] === 'string' &&\n typeof obj['tag'] === 'string' &&\n typeof obj['updatedAt'] === 'string'\n );\n}\n\nexport async function readSyncEnvelope(): Promise<SyncEnvelope | null> {\n try {\n const raw = await readFile(ICLOUD_SYNC_PATH, 'utf-8');\n const parsed: unknown = JSON.parse(raw);\n if (!isValidEnvelope(parsed)) {\n logger.warn('sync: invalid envelope structure in iCloud file');\n return null;\n }\n return parsed;\n } catch (err: unknown) {\n // File does not exist yet — expected on first sync\n if (\n err instanceof Error &&\n (err as NodeJS.ErrnoException).code === 'ENOENT'\n ) {\n return null;\n }\n logger.warn('sync: could not read iCloud envelope', { error: String(err) });\n return null;\n }\n}\n\nexport async function writeSyncEnvelope(envelope: SyncEnvelope): Promise<void> {\n await mkdir(ICLOUD_SYNC_DIR, { recursive: true });\n const tmpPath = ICLOUD_SYNC_PATH + '.tmp';\n await writeFile(tmpPath, JSON.stringify(envelope, null, 2), {\n encoding: 'utf-8',\n mode: 0o644,\n });\n await rename(tmpPath, ICLOUD_SYNC_PATH);\n}\n"],"mappings":"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","names":["readFile","writeFile","rename","join","join","readFile","writeFile","rename"]}