diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 80859fb0da..6beabbf465 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -84,3 +84,66 @@ jobs: run: | .\${{ matrix.binary }} --version .\${{ matrix.binary }} --help + + test-npm-package: + if: github.event_name == 'pull_request' + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + name: npm package smoke test (${{ matrix.os }}, Node 24) + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '24' + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.12.1 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build + run: pnpm run build + + - name: Pack npm package + run: npm pack + + - name: Verify npx subcommands (Unix) + if: runner.os != 'Windows' + run: | + PACKAGE=$(ls firecrawl-cli-*.tgz | head -n 1) + if [ -z "$PACKAGE" ]; then + echo "Packed package not found" + exit 1 + fi + + PACKAGE_PATH="$PWD/$PACKAGE" + npx -y --package "$PACKAGE_PATH" firecrawl --version + npx -y --package "$PACKAGE_PATH" firecrawl version + npx -y --package "$PACKAGE_PATH" firecrawl login --help + npx -y --package "$PACKAGE_PATH" firecrawl setup --help + npx -y --package "$PACKAGE_PATH" firecrawl help setup + + - name: Verify npx subcommands (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + $Package = Get-ChildItem .\firecrawl-cli-*.tgz | Select-Object -First 1 + if (-not $Package) { + throw "Packed package not found" + } + + npx -y --package $($Package.FullName) firecrawl --version + npx -y --package $($Package.FullName) firecrawl version + npx -y --package $($Package.FullName) firecrawl login --help + npx -y --package $($Package.FullName) firecrawl setup --help + npx -y --package $($Package.FullName) firecrawl help setup diff --git a/package.json b/package.json index f3a2ec3059..2e2cc67339 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firecrawl-cli", - "version": "1.19.1", + "version": "1.19.2", "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__/cli-argv.test.ts b/src/__tests__/cli-argv.test.ts new file mode 100644 index 0000000000..c6a347fda1 --- /dev/null +++ b/src/__tests__/cli-argv.test.ts @@ -0,0 +1,32 @@ +import { spawnSync } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; + +describe('CLI argv parsing', () => { + const cliPath = resolve(process.cwd(), 'dist/index.js'); + const testWithBuiltCli = existsSync(cliPath) ? it : it.skip; + + testWithBuiltCli( + 'parses subcommands when a wrapper leaves the entry script path in argv', + () => { + const script = ` + process.argv.splice(1, 0, ${JSON.stringify(cliPath)}); + require(process.argv[1]); + `; + + const result = spawnSync( + process.execPath, + ['-e', script, 'setup', '--help'], + { + cwd: process.cwd(), + encoding: 'utf8', + } + ); + + expect(result.status).toBe(0); + expect(result.stdout).toContain('Usage: firecrawl setup'); + expect(result.stderr).not.toContain('unknown command'); + } + ); +}); diff --git a/src/index.ts b/src/index.ts index 2b63ddf317..1927862e21 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1907,11 +1907,12 @@ program collectTopLevelCommands(); -// Parse arguments -const args = process.argv.slice(2); - // Handle the main entry point async function main() { + // Parse user arguments explicitly instead of letting Commander infer whether + // argv came from node, eval, electron, or another wrapper. + const args = process.argv.slice(2); + // Handle --version with --auth-status before Commander processes it // Commander's built-in --version handler doesn't support additional flags const hasVersion = args.includes('--version') || args.includes('-V'); @@ -1991,20 +1992,13 @@ async function main() { // Modify argv to include scrape command with URL and formats as positional arguments // This allows commander to parse it normally with all hooks and options - const modifiedArgv = [ - process.argv[0], - process.argv[1], - 'scrape', - url, - ...positionalFormats, - ...otherArgs, - ]; + const modifiedArgs = ['scrape', url, ...positionalFormats, ...otherArgs]; // Parse using the main program (which includes hooks and global options) - await program.parseAsync(modifiedArgv); + await program.parseAsync(modifiedArgs, { from: 'user' }); } else { // Normal command parsing - await program.parseAsync(); + await program.parseAsync(args, { from: 'user' }); } }