Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "firecrawl-cli",
"version": "1.19.17",
"version": "1.19.18",
"description": "Command-line interface for Firecrawl. Scrape, crawl, and extract data from any website directly from your terminal.",
"main": "dist/index.js",
"bin": {
Expand Down
181 changes: 148 additions & 33 deletions src/__tests__/commands/launch.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import { spawnSync } from 'child_process';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { select } from '@inquirer/prompts';
import { handleLaunchCommand } from '../../commands/launch';
import {
installHermesMcp,
installMcp,
installOpenClawMcp,
installSkillsForAgent,
} from '../../commands/setup';
import { ALL_SKILL_REPOS } from '../../commands/skills-install';

vi.mock('child_process', () => ({
spawnSync: vi.fn(),
}));

vi.mock('@inquirer/prompts', () => ({
select: vi.fn(),
}));

vi.mock('../../commands/setup', () => ({
installHermesMcp: vi.fn(async () => undefined),
installMcp: vi.fn(async () => undefined),
Expand All @@ -20,23 +26,57 @@ vi.mock('../../commands/setup', () => ({
}));

describe('handleLaunchCommand', () => {
const originalIsTty = process.stdin.isTTY;

beforeEach(() => {
vi.clearAllMocks();
vi.mocked(spawnSync).mockReturnValue({ status: 0 } as never);
Object.defineProperty(process.stdin, 'isTTY', {
configurable: true,
value: false,
});
});

afterEach(() => {
Object.defineProperty(process.stdin, 'isTTY', {
configurable: true,
value: originalIsTty,
});
});

function setStdinTty(value: boolean): () => void {
const originalIsTty = process.stdin.isTTY;
Object.defineProperty(process.stdin, 'isTTY', {
configurable: true,
value,
});
return () => {
Object.defineProperty(process.stdin, 'isTTY', {
configurable: true,
value: originalIsTty,
});
};
}

it('installs Claude Code MCP without launching in install mode', async () => {
await handleLaunchCommand('claude', { install: true });

expect(installMcp).toHaveBeenCalledWith({
agent: 'claude-code',
global: true,
yes: true,
quiet: true,
});
expect(installSkillsForAgent).toHaveBeenCalledWith('claude-code', {
global: true,
yes: true,
});
expect(installSkillsForAgent).toHaveBeenCalledWith(
'claude-code',
{
global: true,
yes: true,
nativeSkills: true,
quiet: true,
},
ALL_SKILL_REPOS
);
expect(spawnSync).not.toHaveBeenCalled();
});

Expand All @@ -56,6 +96,7 @@ describe('handleLaunchCommand', () => {
agent: 'vscode',
global: true,
yes: true,
quiet: true,
});
expect(installSkillsForAgent).not.toHaveBeenCalled();
expect(spawnSync).toHaveBeenNthCalledWith(1, 'code', ['--version'], {
Expand All @@ -76,11 +117,18 @@ describe('handleLaunchCommand', () => {
agent: 'codex',
global: true,
yes: true,
quiet: true,
});
expect(installSkillsForAgent).toHaveBeenCalledWith('codex', {
global: true,
yes: true,
});
expect(installSkillsForAgent).toHaveBeenCalledWith(
'codex',
{
global: true,
yes: true,
nativeSkills: true,
quiet: true,
},
ALL_SKILL_REPOS
);
expect(spawnSync).toHaveBeenNthCalledWith(
2,
'codex',
Expand All @@ -89,18 +137,74 @@ describe('handleLaunchCommand', () => {
);
});

it('configures Codex MCP and opens Codex App separately from the CLI', async () => {
await handleLaunchCommand('codex-app');
it('asks which Codex setup to run and can install MCP only', async () => {
const restoreStdin = setStdinTty(true);
vi.mocked(select).mockResolvedValue('mcp');

try {
await handleLaunchCommand('codex', { install: true });
} finally {
restoreStdin();
}

expect(select).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Configure Firecrawl for Codex',
})
);
expect(installMcp).toHaveBeenCalledWith({
agent: 'codex',
global: true,
yes: true,
quiet: true,
});
expect(installSkillsForAgent).toHaveBeenCalledWith('codex', {
expect(installSkillsForAgent).not.toHaveBeenCalled();
expect(spawnSync).not.toHaveBeenCalled();
});

it('asks which Codex setup to run and can install CLI skills only', async () => {
const restoreStdin = setStdinTty(true);
vi.mocked(select).mockResolvedValue('skills');

try {
await handleLaunchCommand('codex', { install: true });
} finally {
restoreStdin();
}

expect(installMcp).not.toHaveBeenCalled();
expect(installSkillsForAgent).toHaveBeenCalledWith(
'codex',
{
global: true,
yes: true,
nativeSkills: true,
quiet: true,
},
ALL_SKILL_REPOS
);
expect(spawnSync).not.toHaveBeenCalled();
});

it('configures Codex MCP and opens Codex App separately from the CLI', async () => {
await handleLaunchCommand('codex-app');

expect(installMcp).toHaveBeenCalledWith({
agent: 'codex',
global: true,
yes: true,
quiet: true,
});
expect(installSkillsForAgent).toHaveBeenCalledWith(
'codex',
{
global: true,
yes: true,
nativeSkills: true,
quiet: true,
},
ALL_SKILL_REPOS
);
expect(spawnSync).toHaveBeenNthCalledWith(1, 'open', ['--version'], {
stdio: 'ignore',
});
Expand All @@ -122,10 +226,16 @@ describe('handleLaunchCommand', () => {
await handleLaunchCommand('opencode', { skipMcp: true });

expect(installMcp).not.toHaveBeenCalled();
expect(installSkillsForAgent).toHaveBeenCalledWith('opencode', {
global: true,
yes: true,
});
expect(installSkillsForAgent).toHaveBeenCalledWith(
'opencode',
{
global: true,
yes: true,
nativeSkills: true,
quiet: true,
},
ALL_SKILL_REPOS
);
expect(spawnSync).toHaveBeenNthCalledWith(1, 'opencode', ['--version'], {
stdio: 'ignore',
});
Expand All @@ -142,10 +252,16 @@ describe('handleLaunchCommand', () => {
await handleLaunchCommand('hermes');

expect(installHermesMcp).toHaveBeenCalled();
expect(installSkillsForAgent).toHaveBeenCalledWith('hermes-agent', {
global: true,
yes: true,
});
expect(installSkillsForAgent).toHaveBeenCalledWith(
'hermes-agent',
{
global: true,
yes: true,
nativeSkills: true,
quiet: true,
},
ALL_SKILL_REPOS
);
expect(spawnSync).toHaveBeenNthCalledWith(1, 'hermes', ['--version'], {
stdio: 'ignore',
});
Expand All @@ -161,10 +277,16 @@ describe('handleLaunchCommand', () => {
await handleLaunchCommand('openclaw');

expect(installOpenClawMcp).toHaveBeenCalled();
expect(installSkillsForAgent).toHaveBeenCalledWith('openclaw', {
global: true,
yes: true,
});
expect(installSkillsForAgent).toHaveBeenCalledWith(
'openclaw',
{
global: true,
yes: true,
nativeSkills: true,
quiet: true,
},
ALL_SKILL_REPOS
);
expect(spawnSync).toHaveBeenNthCalledWith(1, 'openclaw', ['--version'], {
stdio: 'ignore',
});
Expand All @@ -184,21 +306,14 @@ describe('handleLaunchCommand', () => {
});

it('requires an explicit target in non-interactive mode', async () => {
const originalIsTty = process.stdin.isTTY;
Object.defineProperty(process.stdin, 'isTTY', {
configurable: true,
value: false,
});
const restoreStdin = setStdinTty(false);

try {
await expect(handleLaunchCommand()).rejects.toThrow(
'Launch target is required in non-interactive mode'
);
} finally {
Object.defineProperty(process.stdin, 'isTTY', {
configurable: true,
value: originalIsTty,
});
restoreStdin();
}
});
});
27 changes: 25 additions & 2 deletions src/__tests__/commands/setup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import {
handleSetupCommand,
installHermesMcp,
installOpenClawMcp,
installSkillsForAgent,
} from '../../commands/setup';
import { ALL_SKILL_REPOS } from '../../commands/skills-install';
import { configureWebDefaults } from '../../utils/web-defaults';
import { getApiKey } from '../../utils/config';

Expand Down Expand Up @@ -74,6 +76,27 @@ describe('handleSetupCommand', () => {
);
});

it('installs all skill repos for Codex non-interactively', async () => {
await installSkillsForAgent(
'codex',
{ global: true, yes: true },
ALL_SKILL_REPOS
);

expect(execSync).toHaveBeenCalledWith(
'npx -y skills add firecrawl/cli --full-depth --global --yes --agent codex',
expect.objectContaining({ stdio: 'inherit' })
);
expect(execSync).toHaveBeenCalledWith(
'npx -y skills add firecrawl/skills --full-depth --global --yes --agent codex',
expect.objectContaining({ stdio: 'inherit' })
);
expect(execSync).toHaveBeenCalledWith(
'npx -y skills add firecrawl/firecrawl-workflows --full-depth --global --yes --agent codex',
expect.objectContaining({ stdio: 'inherit' })
);
});

it('configures Firecrawl as the default web provider via make default', async () => {
await handleMakeDefaultCommand({ yes: true });

Expand All @@ -87,11 +110,11 @@ describe('handleSetupCommand', () => {
await handleSetupCommand(undefined, { yes: true });

expect(execSync).toHaveBeenCalledWith(
'npx -y skills add firecrawl/cli --full-depth --global --all',
'npx -y skills add firecrawl/cli --full-depth --global --all --yes',
expect.objectContaining({ stdio: 'inherit' })
);
expect(execSync).toHaveBeenCalledWith(
'npx -y skills add firecrawl/skills --full-depth --global --all',
'npx -y skills add firecrawl/skills --full-depth --global --all --yes',
expect.objectContaining({ stdio: 'inherit' })
);
expect(execSync).toHaveBeenCalledWith(
Expand Down
Loading
Loading