diff --git a/CHANGELOG.md b/CHANGELOG.md index aedd21dd..caadeeff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixes +- Homebrew: make the tap formula fail clearly on Linux instead of installing a macOS binary, and add generator/test coverage for the macOS-only guard (#147, thanks @steipete). - Firecrawl: reject `--firecrawl always` for YouTube URLs with an explicit guidance error instead of silently skipping Firecrawl on the transcript-first path (#145, thanks @steipete). - YouTube: keep Gemini-only no-caption runs on the transcription path by forwarding the Google API key from the top-level URL flow into link-preview transcription config (#148, thanks @bytrangle). - Daemon tests: expand CORS allowlist edge-case coverage for localhost variants, extension-origin casing, spoofed localhost domains, and full trusted response headers (#142, thanks @sebastiondev). diff --git a/docs/chrome-extension.md b/docs/chrome-extension.md index 1676ba97..3237b29a 100644 --- a/docs/chrome-extension.md +++ b/docs/chrome-extension.md @@ -12,7 +12,7 @@ Quickstart: - Install summarize (choose one): - `npm i -g @steipete/summarize` - - `brew install steipete/tap/summarize` (macOS arm64) + - `brew install steipete/tap/summarize` (macOS only) - Build/load extension: `apps/chrome-extension/README.md` - Firefox sidebar build: `pnpm -C apps/chrome-extension build:firefox` (load via `about:debugging` → temporary add-on) - Open side panel → copy token install command → run: diff --git a/docs/releasing.md b/docs/releasing.md index 5f6a47b6..bdb14332 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -8,7 +8,7 @@ summary: "Release checklist + Homebrew tap update." - Ship npm packages (core first, then CLI). - Tag + GitHub release. -- Update Homebrew tap so `brew install steipete/tap/summarize` matches latest tag. +- Update the macOS-only Homebrew tap so `brew install steipete/tap/summarize` matches latest tag. ## Checklist @@ -17,10 +17,12 @@ summary: "Release checklist + Homebrew tap update." 3. If you didn’t run `tap` in the script, update the Homebrew tap formula for `summarize`: - Bump version to the new tag. - Update tarball URL + SHA256 for the new release. + - Keep the formula guarded as macOS-only; Linux installs must fail clearly and point users to npm until Linux artifacts exist. 4. Verify Homebrew install reflects the new version: - - `brew install steipete/tap/summarize` - - `summarize --version` matches tag. - - Run a feature added in the release (e.g. `summarize daemon install` for v0.8.2). + - macOS: `brew install steipete/tap/summarize` + - macOS: `summarize --version` matches tag. + - macOS: run a feature added in the release (e.g. `summarize daemon install` for v0.8.2). + - Linux: `brew install steipete/tap/summarize` fails with the explicit macOS-only / npm guidance. 5. If anything fails, fix and re-cut the release (no partials). ## Common failure diff --git a/scripts/release-formula.js b/scripts/release-formula.js index 2b935cb3..7c831f20 100644 --- a/scripts/release-formula.js +++ b/scripts/release-formula.js @@ -1,49 +1,107 @@ import { readFileSync, writeFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; -function replaceOnce(input, pattern, replacer) { - const next = input.replace(pattern, replacer); - if (next === input) { - throw new Error(`failed to update formula using pattern: ${pattern}`); +export const LINUX_HOMEBREW_MESSAGE = + "summarize Homebrew formula is macOS-only; use npm install -g @steipete/summarize on Linux"; + +function skipDoBlock(lines, start) { + let depth = 0; + for (let index = start; index < lines.length; index += 1) { + const trimmed = lines[index].trim(); + if (trimmed.endsWith(" do")) { + depth += 1; + continue; + } + if (trimmed === "end") { + depth -= 1; + if (depth === 0) { + return index + 1; + } + } } - return next; + throw new Error(`failed to find matching end for block starting at line ${start + 1}`); } -export function updateFormulaForMacArtifacts(data, { urlArm, shaArm, urlX64, shaX64 }) { - if (data.includes("on_arm do") && data.includes("on_intel do")) { - let next = data; - next = replaceOnce(next, /(on_arm do\s*\n\s*url ")(.*?)(")/s, `$1${urlArm}$3`); - next = replaceOnce(next, /(on_arm do.*?\n\s*sha256 ")(.*?)(")/s, `$1${shaArm}$3`); - next = replaceOnce(next, /(on_intel do\s*\n\s*url ")(.*?)(")/s, `$1${urlX64}$3`); - next = replaceOnce(next, /(on_intel do.*?\n\s*sha256 ")(.*?)(")/s, `$1${shaX64}$3`); - return next; - } +function stripExistingPlatformConfig(data) { + const lines = data.split("\n"); + const next = []; - if (data.includes("depends_on arch: :arm64")) { - const dualBlock = [ - " on_arm do", - ` url "${urlArm}"`, - ` sha256 "${shaArm}"`, - " end", - "", - " on_intel do", - ` url "${urlX64}"`, - ` sha256 "${shaX64}"`, - " end", - ].join("\n"); - - let next = data; - next = replaceOnce(next, / url "[^"\n]+"\n sha256 "[^"\n]+"/, dualBlock); - next = replaceOnce(next, /^ depends_on arch: :arm64\s*\n?/m, ""); - return next; + for (let index = 0; index < lines.length; ) { + const line = lines[index]; + if (/^ on_(macos|linux|arm|intel) do$/.test(line)) { + index = skipDoBlock(lines, index); + continue; + } + if ( + /^ url "[^"\n]+"$/.test(line) || + /^ sha256 "[^"\n]+"$/.test(line) || + /^ depends_on arch: :arm64$/.test(line) + ) { + index += 1; + continue; + } + next.push(line); + index += 1; } - let next = data; - next = replaceOnce(next, /^ url ".*"$/m, ` url "${urlArm}"`); - next = replaceOnce(next, /^ sha256 ".*"$/m, ` sha256 "${shaArm}"`); return next; } +function findPlatformInsertIndex(lines) { + let insertIndex = 1; + for (let index = 1; index < lines.length; index += 1) { + if (/^ (desc|homepage|version|license) /.test(lines[index])) { + insertIndex = index + 1; + continue; + } + if (lines[index].trim() === "") { + continue; + } + break; + } + return insertIndex; +} + +function buildPlatformBlock({ urlArm, shaArm, urlX64, shaX64 }) { + return [ + " on_macos do", + " on_arm do", + ` url "${urlArm}"`, + ` sha256 "${shaArm}"`, + " end", + "", + " on_intel do", + ` url "${urlX64}"`, + ` sha256 "${shaX64}"`, + " end", + " end", + "", + " on_linux do", + ` odie "${LINUX_HOMEBREW_MESSAGE}"`, + " end", + ]; +} + +export function updateFormulaForMacArtifacts(data, { urlArm, shaArm, urlX64, shaX64 }) { + const lines = stripExistingPlatformConfig(data); + const insertIndex = findPlatformInsertIndex(lines); + const output = [ + ...lines.slice(0, insertIndex), + "", + ...buildPlatformBlock({ urlArm, shaArm, urlX64, shaX64 }), + "", + ...lines.slice(insertIndex).filter((line, index, array) => { + if (line.trim() !== "") return true; + const previous = index > 0 ? array[index - 1] : ""; + return previous.trim() !== ""; + }), + ]; + return `${output + .join("\n") + .replace(/\n{3,}/g, "\n\n") + .trimEnd()}\n`; +} + export function updateFormulaFile(path, args) { const data = readFileSync(path, "utf8"); writeFileSync(path, updateFormulaForMacArtifacts(data, args)); diff --git a/tests/release.formula.test.ts b/tests/release.formula.test.ts index 864e3e80..69b8173c 100644 --- a/tests/release.formula.test.ts +++ b/tests/release.formula.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from "vitest"; -import { updateFormulaForMacArtifacts } from "../scripts/release-formula.js"; +import { + LINUX_HOMEBREW_MESSAGE, + updateFormulaForMacArtifacts, +} from "../scripts/release-formula.js"; const urls = { urlArm: "https://example.com/summarize-macos-arm64.tar.gz", @@ -25,15 +28,18 @@ end const output = updateFormulaForMacArtifacts(input, urls); + expect(output).toContain("on_macos do"); + expect(output).toContain("on_linux do"); expect(output).toContain(`url "${urls.urlArm}"`); expect(output).toContain(`sha256 "${urls.shaArm}"`); expect(output).toContain(`url "${urls.urlX64}"`); expect(output).toContain(`sha256 "${urls.shaX64}"`); + expect(output).toContain(`odie "${LINUX_HOMEBREW_MESSAGE}"`); expect(output).not.toContain("old-arm"); expect(output).not.toContain("old-x64"); }); - it("converts arm64-only formulas to on_arm/on_intel blocks", () => { + it("converts arm64-only formulas to macOS-only dual-arch blocks", () => { const input = `class Summarize < Formula desc "summarize" homepage "https://example.com" @@ -45,8 +51,10 @@ end const output = updateFormulaForMacArtifacts(input, urls); + expect(output).toContain("on_macos do"); expect(output).toContain("on_arm do"); expect(output).toContain("on_intel do"); + expect(output).toContain("on_linux do"); expect(output).toContain(`url "${urls.urlArm}"`); expect(output).toContain(`sha256 "${urls.shaArm}"`); expect(output).toContain(`url "${urls.urlX64}"`); @@ -54,7 +62,7 @@ end expect(output).not.toContain("depends_on arch: :arm64"); }); - it("keeps fallback formulas arm64-only when no arch blocks exist", () => { + it("rewrites fallback formulas to a canonical macOS-only shape", () => { const input = `class Summarize < Formula url "https://old.example/default.tgz" sha256 "old-default" @@ -63,9 +71,12 @@ end const output = updateFormulaForMacArtifacts(input, urls); + expect(output).toContain("on_macos do"); + expect(output).toContain("on_linux do"); expect(output).toContain(`url "${urls.urlArm}"`); expect(output).toContain(`sha256 "${urls.shaArm}"`); - expect(output).not.toContain(urls.urlX64); - expect(output).not.toContain(urls.shaX64); + expect(output).toContain(`url "${urls.urlX64}"`); + expect(output).toContain(`sha256 "${urls.shaX64}"`); + expect(output).toContain(`odie "${LINUX_HOMEBREW_MESSAGE}"`); }); });