From ac90d047aaf07541e7900aef5900eaba90601fae Mon Sep 17 00:00:00 2001 From: Developers Digest <124798203+developersdigest@users.noreply.github.com> Date: Tue, 30 Jun 2026 22:05:40 -0400 Subject: [PATCH] Support conditional implicit stdin for monitor command Allow monitor create/update to only read piped stdin when flags can't build a payload. Added allowImplicitStdin option to readJsonPayload and exported helpers hasCreateFlagPayload / hasUpdateFlagPayload to detect when flags are sufficient. Updated command actions to pass the allowImplicitStdin flag and extended tests to cover the stdin payload policy. Also bumped package version to 1.19.23. --- package.json | 2 +- src/__tests__/commands/monitor.test.ts | 31 +++++++++- src/commands/monitor.ts | 80 ++++++++++++++++++++++++-- 3 files changed, 106 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index f13c0be40..e9fd0d728 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firecrawl-cli", - "version": "1.19.22", + "version": "1.19.23", "description": "Command-line interface for Firecrawl. Scrape, crawl, and extract data from any website directly from your terminal.", "main": "dist/index.js", "bin": { diff --git a/src/__tests__/commands/monitor.test.ts b/src/__tests__/commands/monitor.test.ts index 715bdef0e..fc8d258a4 100644 --- a/src/__tests__/commands/monitor.test.ts +++ b/src/__tests__/commands/monitor.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from 'vitest'; -import { buildCreateBody } from '../../commands/monitor'; +import { + buildCreateBody, + hasCreateFlagPayload, + hasUpdateFlagPayload, +} from '../../commands/monitor'; describe('monitor command helpers', () => { describe('buildCreateBody', () => { @@ -91,4 +95,29 @@ describe('monitor command helpers', () => { }); }); }); + + describe('stdin payload policy', () => { + it('does not implicitly read stdin when create flags can build a payload', () => { + expect( + hasCreateFlagPayload({ + name: 'LLM releases', + goal: 'Track new model launches', + schedule: 'hourly', + queries: ['OpenAI release'], + }) + ).toBe(true); + }); + + it('still allows implicit stdin for create when no payload flags are present', () => { + expect(hasCreateFlagPayload({ timezone: 'UTC' })).toBe(false); + }); + + it('does not implicitly read stdin when update flags can build a payload', () => { + expect(hasUpdateFlagPayload({ state: 'paused' })).toBe(true); + }); + + it('still allows implicit stdin for update when no update flags are present', () => { + expect(hasUpdateFlagPayload({})).toBe(false); + }); + }); }); diff --git a/src/commands/monitor.ts b/src/commands/monitor.ts index 9c995c3e3..7cf26cf61 100644 --- a/src/commands/monitor.ts +++ b/src/commands/monitor.ts @@ -96,11 +96,18 @@ function emit( * Read a JSON payload from a positional arg or piped stdin. * * - `file` is a path to a .json file, or `-` to read stdin explicitly. - * - If `file` is omitted and stdin is a pipe, stdin is used. + * - If `file` is omitted and stdin is a pipe, stdin is used only when the + * caller did not provide enough flags to build a payload. * - Returns `undefined` when no source is provided — caller falls back to flags. */ -async function readJsonPayload(file?: string): Promise { - if (file === '-' || (!file && !process.stdin.isTTY)) { +async function readJsonPayload( + file?: string, + options: { allowImplicitStdin?: boolean } = {} +): Promise { + if ( + file === '-' || + (!file && options.allowImplicitStdin === true && !process.stdin.isTTY) + ) { const chunks: Buffer[] = []; for await (const chunk of process.stdin) chunks.push(chunk as Buffer); const raw = Buffer.concat(chunks).toString('utf-8').trim(); @@ -114,6 +121,65 @@ async function readJsonPayload(file?: string): Promise { return undefined; } +export function hasCreateFlagPayload(opts: { + name?: string; + goal?: string; + cron?: string; + schedule?: string; + timezone?: string; + page?: string; + scrapeUrls?: string[]; + crawlUrl?: string; + queries?: string[]; + searchWindow?: string; + maxResults?: number; + includeDomains?: string[]; + excludeDomains?: string[]; + webhookUrl?: string; + webhookEvents?: string[]; + email?: string[]; + retentionDays?: number; +}): boolean { + return Boolean( + opts.name || + opts.goal || + opts.cron || + opts.schedule || + opts.page || + (opts.scrapeUrls && opts.scrapeUrls.length > 0) || + opts.crawlUrl || + (opts.queries && opts.queries.length > 0) || + opts.searchWindow || + opts.maxResults !== undefined || + (opts.includeDomains && opts.includeDomains.length > 0) || + (opts.excludeDomains && opts.excludeDomains.length > 0) || + opts.webhookUrl || + (opts.webhookEvents && opts.webhookEvents.length > 0) || + (opts.email && opts.email.length > 0) || + opts.retentionDays !== undefined + ); +} + +export function hasUpdateFlagPayload(opts: { + name?: string; + goal?: string; + cron?: string; + schedule?: string; + timezone?: string; + state?: string; + retentionDays?: number; +}): boolean { + return Boolean( + opts.name || + opts.goal || + opts.cron || + opts.schedule || + opts.timezone || + opts.state || + opts.retentionDays !== undefined + ); +} + function parseCommaList(value: string): string[] { return value .split(',') @@ -340,7 +406,9 @@ whole web each check and alerts on new results matching your --goal (required wi ) ).action(async (file: string | undefined, options) => { try { - const fromJson = await readJsonPayload(file); + const fromJson = await readJsonPayload(file, { + allowImplicitStdin: !hasCreateFlagPayload(options), + }); const body = fromJson ?? buildCreateBody({ @@ -437,7 +505,9 @@ whole web each check and alerts on new results matching your --goal (required wi .option('--retention-days ', 'Snapshot retention window', parseInt) ).action(async (monitorId: string, file: string | undefined, options) => { try { - const fromJson = await readJsonPayload(file); + const fromJson = await readJsonPayload(file, { + allowImplicitStdin: !hasUpdateFlagPayload(options), + }); let body: Record; if (fromJson) { body = fromJson as Record;