brew-tui 0.9.1 → 0.9.2

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
@@ -6,7 +6,7 @@
6
6
  [![Node](https://img.shields.io/badge/node-%3E%3D22-brightgreen)](https://nodejs.org/)
7
7
  [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
8
8
  [![Homebrew](https://img.shields.io/badge/homebrew-tap-orange)](https://github.com/MoLinesDesigns/homebrew-tap)
9
- [![Tests](https://img.shields.io/badge/tests-211%20passing-brightgreen)]()
9
+ [![Tests](https://img.shields.io/badge/tests-434%20passing-brightgreen)]()
10
10
 
11
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
 
@@ -112,14 +112,17 @@ brew-tui delete-account # Remove all local data (~/.brew-tui/)
112
112
 
113
113
  ### Keyboard Navigation
114
114
 
115
- > **Heads up — keyboard model changed in 0.9.0.** Numbers `1`–`0` no longer
116
- > jump between views; they now run the numbered actions in the footer of the
117
- > current view. Use `m` to focus the side menu and navigate with arrows.
118
- > Old per-view letter shortcuts (`i`, `u`, `r`, …) still work as aliases.
115
+ > **Heads up — keyboard model changed in 0.9.0 / 0.9.1.** Numbers `1`–`0` no
116
+ > longer jump between views; they now run the numbered actions in the footer
117
+ > of the current view. The side menu opens automatically on launch (0.9.1) so
118
+ > arrows operate it from the first frame; press `m` to close or reopen it.
119
+ > The blinking orange `M` in the menu indicator marks the toggle. Old per-view
120
+ > letter shortcuts (`i`, `u`, `r`, …) still work as aliases.
119
121
 
120
122
  | Key | Action |
121
123
  |-----|--------|
122
- | `m` | Open the side menu (then `↑`/`↓` + `Enter`, `Esc`/`m` to close) |
124
+ | `↑` / `↓` + `Enter` | Operate the side menu (active by default on launch) |
125
+ | `m` | Toggle the side menu (close / reopen) |
123
126
  | `1`-`9` | Run the matching numbered action in the current view's footer |
124
127
  | `↑` / `↓` (or `j` / `k`) | Move within a list |
125
128
  | `Enter` | Open / confirm the highlighted item |
@@ -190,7 +193,7 @@ Views (React/Ink) --> Stores (Zustand) --> brew-api --> Parsers --> brew CLI (sp
190
193
  - ESM-only, TypeScript strict mode, built with [tsup](https://github.com/egoist/tsup)
191
194
  - All streaming operations (install, upgrade) use AsyncGenerators yielding lines in real time
192
195
  - Package names validated via regex before passing to `spawn` (no shell injection)
193
- - 211 tests across 20 suites (Vitest)
196
+ - 434 tests across 59 suites (Vitest)
194
197
 
195
198
  ---
196
199
 
@@ -242,7 +245,7 @@ cd Brew-TUI
242
245
  npm install
243
246
  npm run dev # Run with tsx (requires interactive TTY)
244
247
  npm run typecheck # tsc --noEmit
245
- npm run test # vitest (211 tests)
248
+ npm run test # vitest (434 tests)
246
249
  npm run lint # eslint
247
250
  npm run build # Production bundle via tsup
248
251
  ```
@@ -35,7 +35,6 @@ async function installBrewBar(isPro, force = false) {
35
35
  if (!force && await isBrewBarInstalled()) {
36
36
  throw new Error(t("cli_brewbarAlreadyInstalled"));
37
37
  }
38
- console.log(t("cli_brewbarInstalling"));
39
38
  const TMP_ZIP = join(tmpdir(), "BrewBar-" + randomUUID() + ".zip");
40
39
  const res = await fetchWithTimeout(DOWNLOAD_URL, {}, 12e4);
41
40
  if (!res.ok || !res.body) {
@@ -125,4 +124,4 @@ export {
125
124
  launchBrewBar,
126
125
  uninstallBrewBar
127
126
  };
128
- //# sourceMappingURL=brewbar-installer-V6R7BORH.js.map
127
+ //# sourceMappingURL=brewbar-installer-GWJ76J6G.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/MoLinesDesigns/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 // 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":[]}
package/build/index.js CHANGED
@@ -4725,7 +4725,7 @@ function AccountView() {
4725
4725
  status === "pro" || status === "team" || status === "expired" ? `v ${t("hint_revalidate")} ` : "",
4726
4726
  revalidating ? t("account_revalidating") : "",
4727
4727
  " ",
4728
- t("app_version", { version: "0.9.1" })
4728
+ t("app_version", { version: "0.9.2" })
4729
4729
  ] }) })
4730
4730
  ] });
4731
4731
  }
@@ -6133,7 +6133,7 @@ async function reportError(err, context = {}) {
6133
6133
  const config = await resolveConfig();
6134
6134
  if (!config.enabled || !config.endpoint) return;
6135
6135
  const machineId = await getMachineId();
6136
- const version = true ? "0.9.1" : "unknown";
6136
+ const version = true ? "0.9.2" : "unknown";
6137
6137
  await postReport(buildReport("error", err, context, machineId, version), config);
6138
6138
  }
6139
6139
  async function installCrashReporter() {
@@ -6142,7 +6142,7 @@ async function installCrashReporter() {
6142
6142
  if (!config.enabled || !config.endpoint) return;
6143
6143
  _installed = true;
6144
6144
  const machineId = await getMachineId();
6145
- const version = true ? "0.9.1" : "unknown";
6145
+ const version = true ? "0.9.2" : "unknown";
6146
6146
  process.on("uncaughtException", (err) => {
6147
6147
  void postReport(buildReport("fatal", err, { kind: "uncaughtException" }, machineId, version), config);
6148
6148
  });
@@ -6157,7 +6157,7 @@ import { jsx as jsx38 } from "react/jsx-runtime";
6157
6157
  var [, , command, arg] = process.argv;
6158
6158
  async function runCli() {
6159
6159
  if (command === "--version" || command === "-v" || command === "version") {
6160
- process.stdout.write("0.9.1\n");
6160
+ process.stdout.write("0.9.2\n");
6161
6161
  return;
6162
6162
  }
6163
6163
  await ensureDataDirs();
@@ -6296,8 +6296,9 @@ Snapshots: ${snapshots.length} (latest: ${latest ? formatDate(latest.capturedAt)
6296
6296
  if (command === "install-brewbar") {
6297
6297
  await useLicenseStore.getState().initialize();
6298
6298
  const isPro = useLicenseStore.getState().isPro();
6299
- const { installBrewBar } = await import("./brewbar-installer-V6R7BORH.js");
6299
+ const { installBrewBar } = await import("./brewbar-installer-GWJ76J6G.js");
6300
6300
  try {
6301
+ console.log(t("cli_brewbarInstalling"));
6301
6302
  await installBrewBar(isPro, arg === "--force");
6302
6303
  console.log(t("cli_brewbarInstalled"));
6303
6304
  } catch (err) {
@@ -6307,7 +6308,7 @@ Snapshots: ${snapshots.length} (latest: ${latest ? formatDate(latest.capturedAt)
6307
6308
  return;
6308
6309
  }
6309
6310
  if (command === "uninstall-brewbar") {
6310
- const { uninstallBrewBar } = await import("./brewbar-installer-V6R7BORH.js");
6311
+ const { uninstallBrewBar } = await import("./brewbar-installer-GWJ76J6G.js");
6311
6312
  try {
6312
6313
  await uninstallBrewBar();
6313
6314
  console.log(t("cli_brewbarUninstalled"));
@@ -6338,8 +6339,8 @@ async function ensureBrewBarRunning() {
6338
6339
  if (process.platform !== "darwin") return;
6339
6340
  await useLicenseStore.getState().initialize();
6340
6341
  if (!useLicenseStore.getState().isPro()) return;
6341
- const { isBrewBarInstalled, installBrewBar, launchBrewBar } = await import("./brewbar-installer-V6R7BORH.js");
6342
- const { checkBrewBarVersion } = await import("./version-check-X3HTR3HM.js");
6342
+ const { isBrewBarInstalled, installBrewBar, launchBrewBar } = await import("./brewbar-installer-GWJ76J6G.js");
6343
+ const { checkBrewBarVersion } = await import("./version-check-LHQYDFDA.js");
6343
6344
  try {
6344
6345
  if (!await isBrewBarInstalled()) {
6345
6346
  console.log(t("cli_brewbarInstalling"));