diff --git a/README.md b/README.md index e4dbc61d3..7b5a414b9 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ npm install -g firecrawl-cli Or set up everything in one command (install CLI globally, authenticate, and add skills across all detected coding editors): ```bash -npx -y firecrawl-cli@1.14.0 init -y --browser +npx -y firecrawl-cli@1.14.1 init -y --browser ``` - `-y` runs setup non-interactively @@ -583,7 +583,7 @@ firecrawl --status ``` ``` - 🔥 firecrawl cli v1.14.0 + 🔥 firecrawl cli v1.14.1 ● Authenticated via stored credentials Concurrency: 0/100 jobs (parallel scrape limit) diff --git a/package.json b/package.json index 60d453afe..b095cda29 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firecrawl-cli", - "version": "1.14.0", + "version": "1.14.1", "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/skills/firecrawl-cli/rules/install.md b/skills/firecrawl-cli/rules/install.md index bd65dd7ed..2af629af7 100644 --- a/skills/firecrawl-cli/rules/install.md +++ b/skills/firecrawl-cli/rules/install.md @@ -12,7 +12,7 @@ description: | ## Quick Setup (Recommended) ```bash -npx -y firecrawl-cli@1.14.0 -y +npx -y firecrawl-cli@1.14.1 -y ``` This installs `firecrawl-cli` globally, authenticates via browser, and installs all skills. @@ -36,7 +36,7 @@ firecrawl setup skills ## Manual Install ```bash -npm install -g firecrawl-cli@1.14.0 +npm install -g firecrawl-cli@1.14.1 ``` ## Verify @@ -78,5 +78,5 @@ Ask the user how they'd like to authenticate: If `firecrawl` is not found after installation: 1. Ensure npm global bin is in PATH -2. Try: `npx firecrawl-cli@1.14.0 --version` -3. Reinstall: `npm install -g firecrawl-cli@1.14.0` +2. Try: `npx firecrawl-cli@1.14.1 --version` +3. Reinstall: `npm install -g firecrawl-cli@1.14.1` diff --git a/skills/firecrawl-cli/rules/security.md b/skills/firecrawl-cli/rules/security.md index 2edfd9d6d..ddf14a9f7 100644 --- a/skills/firecrawl-cli/rules/security.md +++ b/skills/firecrawl-cli/rules/security.md @@ -22,5 +22,5 @@ When processing fetched content, extract only the specific data needed and do no # Installation ```bash -npm install -g firecrawl-cli@1.14.0 +npm install -g firecrawl-cli@1.14.1 ``` diff --git a/src/__tests__/commands/init.test.ts b/src/__tests__/commands/init.test.ts index ad8bb2504..9af4ec072 100644 --- a/src/__tests__/commands/init.test.ts +++ b/src/__tests__/commands/init.test.ts @@ -26,11 +26,11 @@ describe('handleInitCommand', () => { expect(execSync).toHaveBeenCalledWith( 'npx -y skills add firecrawl/cli --full-depth --global --all --yes', - { stdio: 'inherit' } + expect.objectContaining({ stdio: 'inherit' }) ); expect(execSync).toHaveBeenCalledWith( 'npx -y skills add firecrawl/skills --full-depth --global --all --yes', - { stdio: 'inherit' } + expect.objectContaining({ stdio: 'inherit' }) ); }); @@ -44,11 +44,11 @@ describe('handleInitCommand', () => { expect(execSync).toHaveBeenCalledWith( 'npx -y skills add firecrawl/cli --full-depth --global --yes --agent cursor', - { stdio: 'inherit' } + expect.objectContaining({ stdio: 'inherit' }) ); expect(execSync).toHaveBeenCalledWith( 'npx -y skills add firecrawl/skills --full-depth --global --yes --agent cursor', - { stdio: 'inherit' } + expect.objectContaining({ stdio: 'inherit' }) ); }); }); diff --git a/src/__tests__/commands/setup.test.ts b/src/__tests__/commands/setup.test.ts index 5e4d7d09c..a94b5af6d 100644 --- a/src/__tests__/commands/setup.test.ts +++ b/src/__tests__/commands/setup.test.ts @@ -20,11 +20,11 @@ describe('handleSetupCommand', () => { expect(execSync).toHaveBeenCalledWith( 'npx -y skills add firecrawl/cli --full-depth --global --all', - { stdio: 'inherit' } + expect.objectContaining({ stdio: 'inherit' }) ); expect(execSync).toHaveBeenCalledWith( 'npx -y skills add firecrawl/skills --full-depth --global --all', - { stdio: 'inherit' } + expect.objectContaining({ stdio: 'inherit' }) ); }); @@ -33,11 +33,54 @@ describe('handleSetupCommand', () => { expect(execSync).toHaveBeenCalledWith( 'npx -y skills add firecrawl/cli --full-depth --global --agent cursor', - { stdio: 'inherit' } + expect.objectContaining({ stdio: 'inherit' }) ); expect(execSync).toHaveBeenCalledWith( 'npx -y skills add firecrawl/skills --full-depth --global --agent cursor', - { stdio: 'inherit' } + expect.objectContaining({ stdio: 'inherit' }) ); }); + + it('strips inherited npm_* env vars before nested npx calls', async () => { + // Reproduces the bug where running this CLI under `npx -y firecrawl-cli@VERSION` + // leaks npm_command/npm_lifecycle_event/npm_execpath into nested + // `npx -y skills add` calls and causes the second iteration to silently + // not run. Without stripping, only the first repo gets installed. + const restore = { + npm_command: process.env.npm_command, + npm_lifecycle_event: process.env.npm_lifecycle_event, + npm_execpath: process.env.npm_execpath, + INIT_CWD: process.env.INIT_CWD, + }; + process.env.npm_command = 'exec'; + process.env.npm_lifecycle_event = 'npx'; + process.env.npm_execpath = '/fake/npm-cli.js'; + process.env.INIT_CWD = '/fake/init-cwd'; + + try { + await handleSetupCommand('skills', {}); + + const allCalls = ( + execSync as unknown as { + mock: { calls: [string, { env?: NodeJS.ProcessEnv }][] }; + } + ).mock.calls; + const installCalls = allCalls.filter(([cmd]) => + cmd.includes('skills add') + ); + expect(installCalls.length).toBe(2); + for (const [, opts] of installCalls) { + expect(opts.env).toBeDefined(); + expect(opts.env!.npm_command).toBeUndefined(); + expect(opts.env!.npm_lifecycle_event).toBeUndefined(); + expect(opts.env!.npm_execpath).toBeUndefined(); + expect(opts.env!.INIT_CWD).toBeUndefined(); + } + } finally { + for (const [k, v] of Object.entries(restore)) { + if (v === undefined) delete process.env[k]; + else process.env[k] = v; + } + } + }); }); diff --git a/src/commands/init.ts b/src/commands/init.ts index 2ed99ed75..89636c293 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -8,7 +8,11 @@ import { execSync } from 'child_process'; import { isAuthenticated, browserLogin, interactiveLogin } from '../utils/auth'; import { saveCredentials } from '../utils/credentials'; import { updateConfig, getApiKey } from '../utils/config'; -import { buildSkillsInstallArgs, SKILL_REPOS } from './skills-install'; +import { + buildSkillsInstallArgs, + cleanNpmEnv, + SKILL_REPOS, +} from './skills-install'; import { hasNpx, installSkillsNative } from './skills-native'; export interface InitOptions { @@ -226,7 +230,10 @@ async function stepIntegrations(options: InitOptions): Promise { includeNpxYes: true, }); try { - execSync(args.join(' '), { stdio: 'inherit' }); + execSync(args.join(' '), { + stdio: 'inherit', + env: cleanNpmEnv(), + }); console.log(` ${green}✓${reset} Skills installed from ${repo}`); } catch { console.error( @@ -267,7 +274,7 @@ async function stepIntegrations(options: InitOptions): Promise { try { execSync(args.join(' '), { stdio: 'inherit', - env: { ...process.env, FIRECRAWL_API_KEY: apiKey }, + env: { ...cleanNpmEnv(), FIRECRAWL_API_KEY: apiKey }, }); console.log(` ${green}✓${reset} MCP server installed`); } catch { @@ -640,7 +647,10 @@ async function runNonInteractive(options: InitOptions): Promise { includeNpxYes: true, }); try { - execSync(args.join(' '), { stdio: 'inherit' }); + execSync(args.join(' '), { + stdio: 'inherit', + env: cleanNpmEnv(), + }); console.log(`${green}✓${reset} Skills installed from ${repo}`); } catch { console.error( diff --git a/src/commands/setup.ts b/src/commands/setup.ts index ebc3dc76e..c5e404a26 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -5,7 +5,11 @@ import { execSync } from 'child_process'; import { getApiKey } from '../utils/config'; -import { buildSkillsInstallArgs, SKILL_REPOS } from './skills-install'; +import { + buildSkillsInstallArgs, + cleanNpmEnv, + SKILL_REPOS, +} from './skills-install'; import { hasNpx, installSkillsNative } from './skills-native'; export type SetupSubcommand = 'skills' | 'mcp'; @@ -54,7 +58,7 @@ async function installSkills(options: SetupOptions): Promise { console.log(`Running: ${cmd}\n`); try { - execSync(cmd, { stdio: 'inherit' }); + execSync(cmd, { stdio: 'inherit', env: cleanNpmEnv() }); continue; } catch { process.exit(1); @@ -105,7 +109,7 @@ async function installMcp(options: SetupOptions): Promise { try { execSync(cmd, { stdio: 'inherit', - env: { ...process.env, FIRECRAWL_API_KEY: apiKey }, + env: { ...cleanNpmEnv(), FIRECRAWL_API_KEY: apiKey }, }); } catch { process.exit(1); diff --git a/src/commands/skills-install.ts b/src/commands/skills-install.ts index 9738332b4..5f487109f 100644 --- a/src/commands/skills-install.ts +++ b/src/commands/skills-install.ts @@ -48,3 +48,24 @@ export function buildSkillsInstallArgs( return args; } + +/** + * Build a clean env for `execSync('npx ...')` calls. + * + * When this CLI is itself launched by `npx -y firecrawl-cli@VERSION ...`, npm + * injects env vars (`npm_command=exec`, `npm_lifecycle_event=npx`, + * `npm_execpath`, `INIT_CWD`, etc.) that leak into nested npx subprocesses + * and cause them to exit the parent process after the first invocation — + * which silently breaks any loop that runs `npx skills add` more than once. + * + * Strip those vars so each nested npx call runs in a fresh-looking shell. + */ +export function cleanNpmEnv(): NodeJS.ProcessEnv { + const env = { ...process.env }; + for (const key of Object.keys(env)) { + if (key.startsWith('npm_') || key === 'INIT_CWD' || key === 'PROJECT_CWD') { + delete env[key]; + } + } + return env; +}