Skip to content

Commit 345f558

Browse files
Merge pull request #137 from firecrawl/launch-commands
feat(launch cmds): Launch commands
2 parents ac63035 + 9d087b9 commit 345f558

7 files changed

Lines changed: 888 additions & 30 deletions

File tree

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "firecrawl-cli",
3-
"version": "1.19.13",
3+
"version": "1.19.15",
44
"description": "Command-line interface for Firecrawl. Scrape, crawl, and extract data from any website directly from your terminal.",
55
"main": "dist/index.js",
66
"bin": {
@@ -72,8 +72,9 @@
7272
},
7373
"dependencies": {
7474
"@inquirer/prompts": "^8.2.1",
75-
"firecrawl": "4.24.0",
7675
"commander": "^14.0.2",
76+
"firecrawl": "4.24.0",
77+
"yaml": "^2.9.0",
7778
"zod-to-json-schema": "3.24.6"
7879
}
7980
}

pnpm-lock.yaml

Lines changed: 15 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import { spawnSync } from 'child_process';
2+
import { beforeEach, describe, expect, it, vi } from 'vitest';
3+
import { handleLaunchCommand } from '../../commands/launch';
4+
import {
5+
installHermesMcp,
6+
installMcp,
7+
installOpenClawMcp,
8+
installSkillsForAgent,
9+
} from '../../commands/setup';
10+
11+
vi.mock('child_process', () => ({
12+
spawnSync: vi.fn(),
13+
}));
14+
15+
vi.mock('../../commands/setup', () => ({
16+
installHermesMcp: vi.fn(async () => undefined),
17+
installMcp: vi.fn(async () => undefined),
18+
installOpenClawMcp: vi.fn(async () => undefined),
19+
installSkillsForAgent: vi.fn(async () => undefined),
20+
}));
21+
22+
describe('handleLaunchCommand', () => {
23+
beforeEach(() => {
24+
vi.clearAllMocks();
25+
vi.mocked(spawnSync).mockReturnValue({ status: 0 } as never);
26+
});
27+
28+
it('installs Claude Code MCP without launching in install mode', async () => {
29+
await handleLaunchCommand('claude', { install: true });
30+
31+
expect(installMcp).toHaveBeenCalledWith({
32+
agent: 'claude-code',
33+
global: true,
34+
yes: true,
35+
});
36+
expect(installSkillsForAgent).toHaveBeenCalledWith('claude-code', {
37+
global: true,
38+
yes: true,
39+
});
40+
expect(spawnSync).not.toHaveBeenCalled();
41+
});
42+
43+
it('supports setup and config as install-mode aliases', async () => {
44+
await handleLaunchCommand('claude', { setup: true });
45+
await handleLaunchCommand('codex', { config: true });
46+
47+
expect(installMcp).toHaveBeenCalledTimes(2);
48+
expect(installSkillsForAgent).toHaveBeenCalledTimes(2);
49+
expect(spawnSync).not.toHaveBeenCalled();
50+
});
51+
52+
it('configures VS Code MCP and launches code with the current workspace', async () => {
53+
await handleLaunchCommand('code');
54+
55+
expect(installMcp).toHaveBeenCalledWith({
56+
agent: 'vscode',
57+
global: true,
58+
yes: true,
59+
});
60+
expect(installSkillsForAgent).not.toHaveBeenCalled();
61+
expect(spawnSync).toHaveBeenNthCalledWith(1, 'code', ['--version'], {
62+
stdio: 'ignore',
63+
});
64+
expect(spawnSync).toHaveBeenNthCalledWith(
65+
2,
66+
'code',
67+
['.'],
68+
expect.objectContaining({ stdio: 'inherit' })
69+
);
70+
});
71+
72+
it('passes extra arguments through to Codex', async () => {
73+
await handleLaunchCommand('codex', {}, ['--sandbox', 'workspace-write']);
74+
75+
expect(installMcp).toHaveBeenCalledWith({
76+
agent: 'codex',
77+
global: true,
78+
yes: true,
79+
});
80+
expect(installSkillsForAgent).toHaveBeenCalledWith('codex', {
81+
global: true,
82+
yes: true,
83+
});
84+
expect(spawnSync).toHaveBeenNthCalledWith(
85+
2,
86+
'codex',
87+
['--sandbox', 'workspace-write'],
88+
expect.objectContaining({ stdio: 'inherit' })
89+
);
90+
});
91+
92+
it('configures Codex MCP and opens Codex App separately from the CLI', async () => {
93+
await handleLaunchCommand('codex-app');
94+
95+
expect(installMcp).toHaveBeenCalledWith({
96+
agent: 'codex',
97+
global: true,
98+
yes: true,
99+
});
100+
expect(installSkillsForAgent).toHaveBeenCalledWith('codex', {
101+
global: true,
102+
yes: true,
103+
});
104+
expect(spawnSync).toHaveBeenNthCalledWith(1, 'open', ['--version'], {
105+
stdio: 'ignore',
106+
});
107+
expect(spawnSync).toHaveBeenNthCalledWith(
108+
2,
109+
'open',
110+
['-b', 'com.openai.codex'],
111+
expect.objectContaining({ stdio: 'inherit' })
112+
);
113+
});
114+
115+
it('does not pass extra arguments to Codex App', async () => {
116+
await expect(
117+
handleLaunchCommand('codex-app', {}, ['--foo'])
118+
).rejects.toThrow('Codex App does not accept extra arguments');
119+
});
120+
121+
it('can launch without touching MCP', async () => {
122+
await handleLaunchCommand('opencode', { skipMcp: true });
123+
124+
expect(installMcp).not.toHaveBeenCalled();
125+
expect(installSkillsForAgent).toHaveBeenCalledWith('opencode', {
126+
global: true,
127+
yes: true,
128+
});
129+
expect(spawnSync).toHaveBeenNthCalledWith(1, 'opencode', ['--version'], {
130+
stdio: 'ignore',
131+
});
132+
});
133+
134+
it('can skip skills for a launch target that normally supports them', async () => {
135+
await handleLaunchCommand('opencode', { skipMcp: true, skipSkills: true });
136+
137+
expect(installMcp).not.toHaveBeenCalled();
138+
expect(installSkillsForAgent).not.toHaveBeenCalled();
139+
});
140+
141+
it('configures Hermes MCP and skills, then launches Hermes Agent', async () => {
142+
await handleLaunchCommand('hermes');
143+
144+
expect(installHermesMcp).toHaveBeenCalled();
145+
expect(installSkillsForAgent).toHaveBeenCalledWith('hermes-agent', {
146+
global: true,
147+
yes: true,
148+
});
149+
expect(spawnSync).toHaveBeenNthCalledWith(1, 'hermes', ['--version'], {
150+
stdio: 'ignore',
151+
});
152+
expect(spawnSync).toHaveBeenNthCalledWith(
153+
2,
154+
'hermes',
155+
[],
156+
expect.objectContaining({ stdio: 'inherit' })
157+
);
158+
});
159+
160+
it('configures OpenClaw MCP and skills, then launches the TUI', async () => {
161+
await handleLaunchCommand('openclaw');
162+
163+
expect(installOpenClawMcp).toHaveBeenCalled();
164+
expect(installSkillsForAgent).toHaveBeenCalledWith('openclaw', {
165+
global: true,
166+
yes: true,
167+
});
168+
expect(spawnSync).toHaveBeenNthCalledWith(1, 'openclaw', ['--version'], {
169+
stdio: 'ignore',
170+
});
171+
expect(spawnSync).toHaveBeenNthCalledWith(
172+
2,
173+
'openclaw',
174+
['tui'],
175+
expect.objectContaining({ stdio: 'inherit' })
176+
);
177+
});
178+
179+
it('can skip skills for Hermes and OpenClaw launch targets', async () => {
180+
await handleLaunchCommand('hermes', { skipSkills: true });
181+
182+
expect(installHermesMcp).toHaveBeenCalled();
183+
expect(installSkillsForAgent).not.toHaveBeenCalled();
184+
});
185+
186+
it('requires an explicit target in non-interactive mode', async () => {
187+
const originalIsTty = process.stdin.isTTY;
188+
Object.defineProperty(process.stdin, 'isTTY', {
189+
configurable: true,
190+
value: false,
191+
});
192+
193+
try {
194+
await expect(handleLaunchCommand()).rejects.toThrow(
195+
'Launch target is required in non-interactive mode'
196+
);
197+
} finally {
198+
Object.defineProperty(process.stdin, 'isTTY', {
199+
configurable: true,
200+
value: originalIsTty,
201+
});
202+
}
203+
});
204+
});

0 commit comments

Comments
 (0)