diff --git a/homebrew/firecrawl-cli.rb b/homebrew/firecrawl-cli.rb deleted file mode 100644 index d27a9171d8..0000000000 --- a/homebrew/firecrawl-cli.rb +++ /dev/null @@ -1,47 +0,0 @@ -class FirecrawlCli < Formula - desc "CLI for Firecrawl - web scraping, web and paper search, browser automation" - homepage "https://firecrawl.dev" - version "1.10.0" - license "ISC" - - on_macos do - on_arm do - url "https://github.com/firecrawl/cli/releases/download/v#{version}/firecrawl-darwin-arm64.tar.gz" - sha256 "PLACEHOLDER" - end - on_intel do - url "https://github.com/firecrawl/cli/releases/download/v#{version}/firecrawl-darwin-x64.tar.gz" - sha256 "PLACEHOLDER" - end - end - on_linux do - on_arm do - url "https://github.com/firecrawl/cli/releases/download/v#{version}/firecrawl-linux-arm64.tar.gz" - sha256 "PLACEHOLDER" - end - on_intel do - url "https://github.com/firecrawl/cli/releases/download/v#{version}/firecrawl-linux-x64.tar.gz" - sha256 "PLACEHOLDER" - end - end - - def install - if OS.mac? - if Hardware::CPU.arm? - bin.install "firecrawl-darwin-arm64" => "firecrawl" - else - bin.install "firecrawl-darwin-x64" => "firecrawl" - end - elsif OS.linux? - if Hardware::CPU.arm? - bin.install "firecrawl-linux-arm64" => "firecrawl" - else - bin.install "firecrawl-linux-x64" => "firecrawl" - end - end - end - - test do - assert_match version.to_s, shell_output("#{bin}/firecrawl --version") - end -end diff --git a/src/__tests__/utils/update-notice.test.ts b/src/__tests__/utils/update-notice.test.ts index e3dce28c94..ab01307aed 100644 --- a/src/__tests__/utils/update-notice.test.ts +++ b/src/__tests__/utils/update-notice.test.ts @@ -6,6 +6,7 @@ import packageJson from '../../../package.json'; import { maybeShowUpdateNotice, formatUpdateNotice, + updateCommand, type UpdateNoticeOptions, } from '../../utils/update-notice'; import { getLatestVersion } from '../../utils/npm-registry'; @@ -62,6 +63,63 @@ describe('update notice', () => { expect(notice.endsWith('╯')).toBe(true); }); + it('suggests brew upgrade for a Homebrew install', () => { + const cellar = + '/opt/homebrew/Cellar/firecrawl-cli/1.26.9/libexec/lib/node_modules/firecrawl-cli/dist/utils'; + + expect(updateCommand([cellar])).toBe('brew upgrade firecrawl-cli'); + expect(formatUpdateNotice('99.99.99', updateCommand([cellar]))).toContain( + 'Run brew upgrade firecrawl-cli to update.' + ); + }); + + it('suggests brew upgrade for a compiled binary in a Homebrew Cellar', () => { + expect( + updateCommand([ + '/$bunfs/root', + '/opt/homebrew/Cellar/firecrawl-cli/1.26.9/bin/firecrawl', + ]) + ).toBe('brew upgrade firecrawl-cli'); + }); + + it('suggests npm for installs outside a Homebrew Cellar', () => { + expect( + updateCommand([ + '/opt/homebrew/lib/node_modules/firecrawl-cli/dist/utils', + '/opt/homebrew/Cellar/node/24.10.0/bin/node', + ]) + ).toBe('npm install -g firecrawl-cli'); + expect( + updateCommand(['/usr/local/lib/node_modules/firecrawl-cli/dist/utils']) + ).toBe('npm install -g firecrawl-cli'); + }); + + it('prints the brew command when installed from Homebrew', async () => { + fs.writeFileSync( + path.join(tmpDir, 'update-check.json'), + JSON.stringify({ + latestVersion: '99.99.99', + checkedAt: new Date('2026-06-04T12:00:00.000Z').toISOString(), + }) + ); + + await maybeShowUpdateNotice({ + cacheDir: tmpDir, + installPaths: [ + '/home/linuxbrew/.linuxbrew/Cellar/firecrawl-cli/1.26.9/libexec/lib/node_modules/firecrawl-cli/dist/utils', + ], + now: new Date('2026-06-04T13:00:00.000Z'), + stderr: stderr(), + }); + + expect(write).toHaveBeenCalledWith( + expect.stringContaining('brew upgrade firecrawl-cli') + ); + expect(write).not.toHaveBeenCalledWith( + expect.stringContaining('npm install -g') + ); + }); + it('aligns borders by terminal columns for emoji, color and combining characters', () => { expect( formatUpdateBox([ diff --git a/src/utils/update-notice.ts b/src/utils/update-notice.ts index 71092523bf..414a6c7a2a 100644 --- a/src/utils/update-notice.ts +++ b/src/utils/update-notice.ts @@ -9,7 +9,8 @@ const CACHE_FILENAME = 'update-check.json'; const CACHE_TTL_MS = 20 * 60 * 60 * 1000; const NOTICE_TTL_MS = 12 * 60 * 60 * 1000; const RELEASE_NOTES_URL = 'https://github.com/firecrawl/cli/releases/latest'; -const INSTALL_COMMAND = 'npm install -g firecrawl-cli'; +const NPM_UPDATE_COMMAND = 'npm install -g firecrawl-cli'; +const BREW_UPDATE_COMMAND = 'brew upgrade firecrawl-cli'; interface UpdateCache { latestVersion: string; @@ -20,6 +21,7 @@ interface UpdateCache { export interface UpdateNoticeOptions { cacheDir?: string; + installPaths?: string[]; now?: Date; stderr?: Pick; } @@ -31,6 +33,18 @@ function getCachePath(cacheDir?: string): string { ); } +/** + * Homebrew installs live under a Cellar keg and must be upgraded with brew. + * Compiled binaries report a virtual __dirname, so the executable path is checked too. + */ +export function updateCommand( + installPaths: string[] = [__dirname, process.execPath] +): string { + return installPaths.some((p) => /[\\/]Cellar[\\/]firecrawl-cli[\\/]/.test(p)) + ? BREW_UPDATE_COMMAND + : NPM_UPDATE_COMMAND; +} + function updateCheckDisabled(): boolean { const value = process.env.FIRECRAWL_NO_UPDATE_CHECK; return value === '1' || value === 'true'; @@ -83,11 +97,14 @@ async function writeCachedLatest( } } -export function formatUpdateNotice(latestVersion: string): string { +export function formatUpdateNotice( + latestVersion: string, + command: string = updateCommand() +): string { const currentVersion = packageJson.version; const lines = [ `✨ Update available! ${currentVersion} -> ${latestVersion}`, - `Run ${INSTALL_COMMAND} to update.`, + `Run ${command} to update.`, '', 'See full release notes:', RELEASE_NOTES_URL, @@ -149,12 +166,17 @@ export async function maybeShowUpdateNotice( if (!shouldShowNotice(latestVersion) || latestVersion === undefined) return; if (noticeWasRecentlyShown(cached, latestVersion, now)) return; + const command = updateCommand(options.installPaths); stderr.write( - `${colorizeUpdateNotice(formatUpdateNotice(latestVersion), stderr, { - title: 'Update available!', - commands: [INSTALL_COMMAND], - url: RELEASE_NOTES_URL, - })}\n\n` + `${colorizeUpdateNotice( + formatUpdateNotice(latestVersion, command), + stderr, + { + title: 'Update available!', + commands: [command], + url: RELEASE_NOTES_URL, + } + )}\n\n` ); await writeCachedLatest(cachePath, { latestVersion,