brew-tui 1.2.2 → 1.2.3

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.
@@ -27,6 +27,30 @@ async function isBrewBarInstalled() {
27
27
  return false;
28
28
  }
29
29
  }
30
+ async function isBrewBarRunning() {
31
+ if (process.platform !== "darwin") return false;
32
+ try {
33
+ const { stdout } = await execFileAsync("pgrep", ["-x", "BrewBar"]);
34
+ return stdout.trim().length > 0;
35
+ } catch {
36
+ return false;
37
+ }
38
+ }
39
+ async function quitBrewBar() {
40
+ if (process.platform !== "darwin") return;
41
+ try {
42
+ await execFileAsync("osascript", ["-e", 'tell application "BrewBar" to quit']);
43
+ } catch {
44
+ }
45
+ for (let i = 0; i < 15; i++) {
46
+ if (!await isBrewBarRunning()) return;
47
+ await new Promise((r) => setTimeout(r, 200));
48
+ }
49
+ try {
50
+ await execFileAsync("pkill", ["-x", "BrewBar"]);
51
+ } catch {
52
+ }
53
+ }
30
54
  async function installBrewBar(isPro, force = false) {
31
55
  if (process.platform !== "darwin") {
32
56
  throw new Error(t("cli_brewbarMacOnly"));
@@ -94,6 +118,10 @@ async function installBrewBar(isPro, force = false) {
94
118
  });
95
119
  throw new Error(t("cli_brewbarDownloadFailed", { error: "SHA-256 checksum unavailable \u2014 cannot verify download integrity" }));
96
120
  }
121
+ const wasRunning = await isBrewBarRunning();
122
+ if (wasRunning) {
123
+ await quitBrewBar();
124
+ }
97
125
  if (force && await isBrewBarInstalled()) {
98
126
  await rm(BREWBAR_APP_PATH, { recursive: true, force: true });
99
127
  }
@@ -105,6 +133,9 @@ async function installBrewBar(isPro, force = false) {
105
133
  await rm(TMP_ZIP, { force: true }).catch(() => {
106
134
  });
107
135
  }
136
+ if (wasRunning) {
137
+ await launchBrewBar();
138
+ }
108
139
  }
109
140
  async function launchBrewBar() {
110
141
  if (process.platform !== "darwin") return;
@@ -123,7 +154,8 @@ async function uninstallBrewBar() {
123
154
  export {
124
155
  installBrewBar,
125
156
  isBrewBarInstalled,
157
+ isBrewBarRunning,
126
158
  launchBrewBar,
127
159
  uninstallBrewBar
128
160
  };
129
- //# sourceMappingURL=brewbar-installer-BKE6Z7OI.js.map
161
+ //# sourceMappingURL=brewbar-installer-2OHLRM27.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\n/// Returns true if the BrewBar process is currently running.\n/// Used by the installer to decide whether to quit + relaunch after an update.\nexport async function isBrewBarRunning(): Promise<boolean> {\n if (process.platform !== 'darwin') return false;\n try {\n const { stdout } = await execFileAsync('pgrep', ['-x', 'BrewBar']);\n // pgrep prints one PID per line when it finds matches; stdout vacío == no proceso.\n return stdout.trim().length > 0;\n } catch {\n // pgrep exits 1 when no match; that means \"not running\", not a failure.\n return false;\n }\n}\n\n/// Asks BrewBar to quit gracefully (LSUIElement → no dialogs), then falls back\n/// to pkill if it hasn't exited within 3 s. Required before reemplazar el bundle:\n/// `ditto -xk` sobre una app en ejecución deja un BrewBar viejo con un Info.plist\n/// nuevo, lo cual confunde el monitor de last-action y los watchers FSEvents.\nasync function quitBrewBar(): Promise<void> {\n if (process.platform !== 'darwin') return;\n try {\n await execFileAsync('osascript', ['-e', 'tell application \"BrewBar\" to quit']);\n } catch {\n /* osascript falla si la app no está registrada; pasamos a pkill */\n }\n for (let i = 0; i < 15; i++) {\n if (!(await isBrewBarRunning())) return;\n await new Promise((r) => setTimeout(r, 200));\n }\n try {\n await execFileAsync('pkill', ['-x', 'BrewBar']);\n } catch {\n /* nada que matar */\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 // Si BrewBar está corriendo, cerrarla antes de tocar el bundle. Sin esto\n // `ditto -xk` sobreescribe los recursos de un proceso vivo y la app queda\n // en estado degradado hasta el próximo lanzamiento.\n const wasRunning = await isBrewBarRunning();\n if (wasRunning) {\n await quitBrewBar();\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 // Si BrewBar estaba corriendo antes de la actualización, relanzarla para\n // que el usuario vuelva a ver el ícono en la menubar sin pasos manuales.\n if (wasRunning) {\n await launchBrewBar();\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
@@ -4668,7 +4668,7 @@ function AccountView() {
4668
4668
  status === "pro" || status === "team" || status === "expired" ? `v ${t("hint_revalidate")} ` : "",
4669
4669
  revalidating ? t("account_revalidating") : "",
4670
4670
  " ",
4671
- t("app_version", { version: "1.2.2" })
4671
+ t("app_version", { version: "1.2.3" })
4672
4672
  ] }) })
4673
4673
  ] });
4674
4674
  }
@@ -6176,7 +6176,7 @@ async function reportError(err, context = {}) {
6176
6176
  const config = await resolveConfig();
6177
6177
  if (!config.enabled || !config.endpoint) return;
6178
6178
  const machineId = await getMachineId();
6179
- const version = true ? "1.2.2" : "unknown";
6179
+ const version = true ? "1.2.3" : "unknown";
6180
6180
  await postReport(buildReport("error", err, context, machineId, version), config);
6181
6181
  }
6182
6182
  async function installCrashReporter() {
@@ -6185,7 +6185,7 @@ async function installCrashReporter() {
6185
6185
  if (!config.enabled || !config.endpoint) return;
6186
6186
  _installed = true;
6187
6187
  const machineId = await getMachineId();
6188
- const version = true ? "1.2.2" : "unknown";
6188
+ const version = true ? "1.2.3" : "unknown";
6189
6189
  process.on("uncaughtException", (err) => {
6190
6190
  void postReport(buildReport("fatal", err, { kind: "uncaughtException" }, machineId, version), config);
6191
6191
  });
@@ -6200,7 +6200,7 @@ import { jsx as jsx39 } from "react/jsx-runtime";
6200
6200
  var [, , command, arg] = process.argv;
6201
6201
  async function runCli() {
6202
6202
  if (command === "--version" || command === "-v" || command === "version") {
6203
- process.stdout.write("1.2.2\n");
6203
+ process.stdout.write("1.2.3\n");
6204
6204
  return;
6205
6205
  }
6206
6206
  await ensureDataDirs();
@@ -6339,7 +6339,7 @@ Snapshots: ${snapshots.length} (latest: ${latest ? formatDate(latest.capturedAt)
6339
6339
  if (command === "install-brewbar") {
6340
6340
  await useLicenseStore.getState().initialize();
6341
6341
  const isPro = useLicenseStore.getState().isPro();
6342
- const { installBrewBar } = await import("./brewbar-installer-BKE6Z7OI.js");
6342
+ const { installBrewBar } = await import("./brewbar-installer-2OHLRM27.js");
6343
6343
  try {
6344
6344
  console.log(t("cli_brewbarInstalling"));
6345
6345
  await installBrewBar(isPro, arg === "--force");
@@ -6351,7 +6351,7 @@ Snapshots: ${snapshots.length} (latest: ${latest ? formatDate(latest.capturedAt)
6351
6351
  return;
6352
6352
  }
6353
6353
  if (command === "uninstall-brewbar") {
6354
- const { uninstallBrewBar } = await import("./brewbar-installer-BKE6Z7OI.js");
6354
+ const { uninstallBrewBar } = await import("./brewbar-installer-2OHLRM27.js");
6355
6355
  try {
6356
6356
  await uninstallBrewBar();
6357
6357
  console.log(t("cli_brewbarUninstalled"));
@@ -6382,8 +6382,8 @@ async function ensureBrewBarRunning() {
6382
6382
  if (process.platform !== "darwin") return;
6383
6383
  await useLicenseStore.getState().initialize();
6384
6384
  if (!useLicenseStore.getState().isPro()) return;
6385
- const { isBrewBarInstalled, installBrewBar, launchBrewBar } = await import("./brewbar-installer-BKE6Z7OI.js");
6386
- const { checkBrewBarVersion } = await import("./version-check-FKY5HGSI.js");
6385
+ const { isBrewBarInstalled, installBrewBar, launchBrewBar } = await import("./brewbar-installer-2OHLRM27.js");
6386
+ const { checkBrewBarVersion } = await import("./version-check-NS6RPUSU.js");
6387
6387
  try {
6388
6388
  if (!await isBrewBarInstalled()) {
6389
6389
  console.log(t("cli_brewbarInstalling"));
@@ -6,7 +6,7 @@ var execFileAsync = promisify(execFile);
6
6
  var BREWBAR_INFO_PLIST = "/Applications/BrewBar.app/Contents/Info.plist";
7
7
  var CONTRACT_VERSION = 1;
8
8
  function expectedVersion() {
9
- return "1.2.2";
9
+ return "1.2.3";
10
10
  }
11
11
  async function readBrewBarVersion() {
12
12
  try {
@@ -61,4 +61,4 @@ export {
61
61
  expectedVersion,
62
62
  readBrewBarVersion
63
63
  };
64
- //# sourceMappingURL=version-check-FKY5HGSI.js.map
64
+ //# sourceMappingURL=version-check-NS6RPUSU.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brew-tui",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "description": "Brew-TUI — Visual TUI for Homebrew package management",
5
5
  "type": "module",
6
6
  "bin": {
@@ -30,7 +30,8 @@
30
30
  "lint": "eslint src/",
31
31
  "typecheck": "tsc --noEmit",
32
32
  "validate": "npm run typecheck && npm run test && npm run build && npm run lint",
33
- "prepublishOnly": "npm run validate",
33
+ "check:brewbar-release": "node scripts/check-brewbar-release.mjs",
34
+ "prepublishOnly": "npm run validate && npm run check:brewbar-release",
34
35
  "prepare": "husky"
35
36
  },
36
37
  "keywords": [
@@ -1 +0,0 @@
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":[]}