From 82e607b18aa88b87b0ae481ce3d357d8d9f53f0b Mon Sep 17 00:00:00 2001 From: Developers Digest <124798203+developersdigest@users.noreply.github.com> Date: Tue, 22 Sep 2026 13:08:05 -0400 Subject: [PATCH 1/2] Fix init package manager selection and partial failure reporting --- src/__tests__/commands/init.test.ts | 89 ++++++++++++ src/__tests__/utils/package-manager.test.ts | 38 ++++++ src/commands/init.ts | 142 ++++++++++++++------ src/utils/package-manager.ts | 34 +++++ 4 files changed, 263 insertions(+), 40 deletions(-) create mode 100644 src/__tests__/utils/package-manager.test.ts create mode 100644 src/utils/package-manager.ts diff --git a/src/__tests__/commands/init.test.ts b/src/__tests__/commands/init.test.ts index 6cf43728ef..37705ac204 100644 --- a/src/__tests__/commands/init.test.ts +++ b/src/__tests__/commands/init.test.ts @@ -37,6 +37,7 @@ vi.mock('@inquirer/prompts', () => ({ describe('handleInitCommand', () => { beforeEach(() => { vi.clearAllMocks(); + process.exitCode = 0; getApiKeyMock.mockReturnValue(undefined); vi.spyOn(console, 'log').mockImplementation(() => {}); vi.spyOn(console, 'error').mockImplementation(() => {}); @@ -44,6 +45,8 @@ describe('handleInitCommand', () => { afterEach(() => { vi.restoreAllMocks(); + vi.unstubAllEnvs(); + process.exitCode = 0; }); it('installs CLI and workflow skills from the catalog globally across all detected agents in non-interactive mode', async () => { @@ -136,3 +139,89 @@ describe('handleInitCommand', () => { ); }); }); + +describe('init global installation', () => { + beforeEach(() => { + vi.mocked(execSync).mockReset(); + process.exitCode = 0; + vi.spyOn(console, 'log').mockImplementation(() => {}); + vi.spyOn(console, 'error').mockImplementation(() => {}); + vi.stubEnv('npm_execpath', ''); + }); + afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllEnvs(); + process.exitCode = 0; + }); + + it.each([ + ['npm', '10.9.0', 'npm install -g firecrawl-cli'], + ['pnpm', '10.12.1', 'pnpm add -g firecrawl-cli'], + ['bun', '1.3.0', 'bun add -g firecrawl-cli'], + ])('uses %s for global installation', async (manager, version, command) => { + vi.stubEnv('npm_config_user_agent', `${manager}/${version}`); + vi.mocked(execSync).mockReturnValue(Buffer.from(version)); + await handleInitCommand({ all: true, skipAuth: true, skipSkills: true }); + expect(execSync).toHaveBeenCalledWith( + command, + expect.objectContaining({ stdio: 'inherit' }) + ); + expect(process.exitCode).toBe(0); + expect(console.log).not.toHaveBeenCalledWith( + expect.stringContaining('Skills installed') + ); + }); + + it('rejects obsolete npm before installation and exits unsuccessfully', async () => { + vi.stubEnv('npm_config_user_agent', 'npm/2.15.12'); + vi.mocked(execSync).mockReturnValue(Buffer.from('2.15.12')); + await handleInitCommand({ all: true, skipAuth: true, skipSkills: true }); + expect(execSync).not.toHaveBeenCalledWith( + 'npm install -g firecrawl-cli', + expect.anything() + ); + expect(console.error).toHaveBeenCalledWith( + expect.stringContaining('Unsupported npm version 2.15.12') + ); + expect(process.exitCode).toBe(1); + }); + + it.each([ + ['npm', 'npx firecrawl-cli'], + ['pnpm', 'pnpm dlx firecrawl-cli'], + ['bun', 'bunx firecrawl-cli'], + ])( + 'reports a failed %s install and preserves successful skills', + async (manager, runner) => { + vi.stubEnv('npm_config_user_agent', `${manager}/10.0.0`); + vi.mocked(execSync).mockImplementation((command) => { + if (String(command).includes(' -g firecrawl-cli')) + throw new Error('install failed'); + return Buffer.from( + String(command).endsWith('--version') + ? '10.0.0' + : 'Installed 14 skills' + ); + }); + await handleInitCommand({ all: true, skipAuth: true }); + expect(process.exitCode).toBe(1); + expect(console.log).toHaveBeenCalledWith( + expect.stringContaining(`${runner} `) + ); + expect(console.log).toHaveBeenCalledWith( + expect.stringContaining('28 skills') + ); + expect(console.error).toHaveBeenCalledWith( + expect.stringContaining( + 'Setup incomplete: global CLI installation failed' + ) + ); + expect(console.error).not.toHaveBeenCalledWith( + expect.stringContaining('sudo') + ); + expect(console.log).not.toHaveBeenCalledWith( + expect.stringContaining('running via npx') + ); + } + ); +}); diff --git a/src/__tests__/utils/package-manager.test.ts b/src/__tests__/utils/package-manager.test.ts new file mode 100644 index 0000000000..e4d5b42204 --- /dev/null +++ b/src/__tests__/utils/package-manager.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from 'vitest'; +import { detectPackageManager } from '../../utils/package-manager'; + +describe('package manager detection', () => { + it.each([ + [{ npm_config_user_agent: 'npm/10.9.0 node/v22.0.0' }, 'npm'], + [{ npm_config_user_agent: 'pnpm/10.12.1 npm/? node/v22.0.0' }, 'pnpm'], + [{ npm_config_user_agent: 'bun/1.3.0' }, 'bun'], + [{ npm_execpath: '/usr/local/lib/node_modules/pnpm/bin/pnpm.cjs' }, 'pnpm'], + [{ npm_execpath: '/home/user/.bun/bin/bun' }, 'bun'], + [{}, 'npm'], + ] as const)('detects %j as %s', (env, expected) => { + expect(detectPackageManager(env, ['node', '/app/firecrawl'])).toBe( + expected + ); + }); + + it.each([ + [ + '/private/tmp/bunx-501-firecrawl-cli@latest/node_modules/firecrawl-cli/dist/index.js', + 'bun', + ], + [ + '/home/user/.cache/pnpm/dlx/123/node_modules/firecrawl-cli/dist/index.js', + 'pnpm', + ], + ])( + 'prefers runner cache path %s over inherited npm metadata', + (entry, expected) => { + expect( + detectPackageManager({ npm_config_user_agent: 'npm/2.15.12' }, [ + 'node', + entry, + ]) + ).toBe(expected); + } + ); +}); diff --git a/src/commands/init.ts b/src/commands/init.ts index 4cf219df2a..4fda5f4f7e 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -5,6 +5,11 @@ */ import { execSync } from 'child_process'; +import { + detectPackageManager, + PACKAGE_MANAGERS, + type PackageManager, +} from '../utils/package-manager'; import { isAuthenticated, browserLogin, interactiveLogin } from '../utils/auth'; import { saveCredentials } from '../utils/credentials'; import { updateConfig, getApiKey } from '../utils/config'; @@ -237,8 +242,7 @@ async function installSkillRepoQuiet( } } - // No npx and native already failed — nothing left to try - return null; + throw new Error('No skills installer is available.'); } /** @@ -298,7 +302,9 @@ function parseSkillCount(output: string): number | null { */ function printNextSteps( skillCount: number | null, - defaultsHandled = false + defaultsHandled = false, + skillsInstalled = true, + cliCommand = 'firecrawl' ): void { const arrow = `${dim}→${reset}`; const summary = @@ -307,34 +313,34 @@ function printNextSteps( : `${green}✓${reset} Skills installed ${dim}across your AI coding agents${reset}`; console.log(''); - console.log(` ${summary}`); + if (skillsInstalled) console.log(` ${summary}`); console.log(''); console.log( ` ${dim}Connect & interact with the web ${reset}${dim}(direct or in your AI agent):${reset}` ); console.log( - ` ${arrow} ${bold}Scrape${reset} "Scrape the pricing page of stripe.com" ${dim}firecrawl scrape https://stripe.com/pricing${reset}` + ` ${arrow} ${bold}Scrape${reset} "Scrape the pricing page of stripe.com" ${dim}${cliCommand} scrape https://stripe.com/pricing${reset}` ); console.log( - ` ${arrow} ${bold}Search${reset} "Search for the latest stories in AI" ${dim}firecrawl search "latest stories in AI"${reset}` + ` ${arrow} ${bold}Search${reset} "Search for the latest stories in AI" ${dim}${cliCommand} search "latest stories in AI"${reset}` ); console.log( - ` ${arrow} ${bold}Interact${reset} "Go to amazon.com, search keyboards, filter by Prime" ${dim}firecrawl interact "search keyboards, filter by Prime"${reset}` + ` ${arrow} ${bold}Interact${reset} "Go to amazon.com, search keyboards, filter by Prime" ${dim}${cliCommand} interact "search keyboards, filter by Prime"${reset}` ); console.log(''); console.log( - ` ${arrow} ${dim}Add MCP: ${reset} ${bold}firecrawl setup mcp${reset}` + ` ${arrow} ${dim}Add MCP: ${reset} ${bold}${cliCommand} setup mcp${reset}` ); if (!defaultsHandled) { console.log( - ` ${arrow} ${dim}Default web:${reset} ${bold}firecrawl setup defaults${reset}` + ` ${arrow} ${dim}Default web:${reset} ${bold}${cliCommand} setup defaults${reset}` ); } console.log( - ` ${arrow} ${dim}Make default:${reset} ${bold}firecrawl make default${reset}` + ` ${arrow} ${dim}Make default:${reset} ${bold}${cliCommand} make default${reset}` ); console.log( - ` ${arrow} ${dim}All commands:${reset} ${bold}firecrawl --help${reset}` + ` ${arrow} ${dim}All commands:${reset} ${bold}${cliCommand} --help${reset}` ); console.log(''); console.log( @@ -358,18 +364,20 @@ function cliIsOnPath(): boolean { } } -/** The directory npm drops global bins into, or null if it can't be resolved. */ -function globalBinDir(): string | null { +/** Resolve the selected package manager's global executable directory. */ +function globalBinDir(manager: PackageManager): string | null { try { - const prefix = execSync('npm prefix -g', { + const prefix = execSync(PACKAGE_MANAGERS[manager].bin, { stdio: ['ignore', 'pipe', 'pipe'], env: cleanNpmEnv(), }) .toString() .trim(); if (!prefix) return null; - // On Windows global bins live in the prefix root; elsewhere in /bin. - return process.platform === 'win32' ? prefix : `${prefix}/bin`; + // npm returns a prefix; pnpm and Bun return the executable directory. + return manager === 'npm' && process.platform !== 'win32' + ? `${prefix}/bin` + : prefix; } catch { return null; } @@ -393,10 +401,10 @@ function rcFileForShell(): string { * fresh Windows shell. The install reports success, yet the command silently * doesn't exist in any new terminal. Detect that and print the exact fix. */ -function warnIfCliNotOnPath(): void { +function warnIfCliNotOnPath(manager: PackageManager): void { if (cliIsOnPath()) return; - const bin = globalBinDir(); + const bin = globalBinDir(manager); console.log(''); console.log( ` ${bold}⚠ "firecrawl" was installed but isn't on your PATH yet.${reset}` @@ -417,11 +425,11 @@ function warnIfCliNotOnPath(): void { console.log(` ${bold}echo '${line}' >> ${rc}${reset}`); } else { console.log( - ` ${dim}Add your npm global bin directory (see "npm prefix -g") to PATH.${reset}` + ` ${dim}Add your global bin directory (see "${PACKAGE_MANAGERS[manager].bin}") to PATH.${reset}` ); } console.log( - ` ${dim}Until then you can still run:${reset} ${bold}npx firecrawl-cli ${reset}` + ` ${dim}Until then you can still run:${reset} ${bold}${PACKAGE_MANAGERS[manager].run} ${reset}` ); console.log(''); } @@ -436,14 +444,49 @@ async function stepInstall(): Promise { if (!shouldInstall) return true; console.log(`\n Installing firecrawl-cli globally...`); + return installGlobalCli(); +} + +function installGlobalCli(): boolean { + const manager = detectPackageManager(); + const commands = PACKAGE_MANAGERS[manager]; try { - execSync('npm install -g firecrawl-cli', { stdio: 'inherit' }); - console.log(` ${green}✓${reset} CLI installed globally\n`); - warnIfCliNotOnPath(); + const version = execSync(`${manager} --version`, { + stdio: ['ignore', 'pipe', 'pipe'], + env: cleanNpmEnv(), + timeout: 10_000, + }) + .toString() + .trim(); + console.log( + ` Using ${manager} ${version} from PATH (Node ${process.version})` + ); + if ( + !/^\d+\.\d+\.\d+/.test(version) || + (manager === 'npm' && Number(version.split('.')[0]) < 7) + ) { + throw new Error( + `Unsupported ${manager} version ${version}. Update ${manager} and check your PATH before retrying.` + ); + } + execSync(commands.install, { stdio: 'inherit', env: cleanNpmEnv() }); + console.log(`${green}✓${reset} CLI installed globally\n`); + warnIfCliNotOnPath(manager); return true; - } catch { + } catch (error) { + console.error(`Failed to install firecrawl-cli globally using ${manager}.`); + console.error( + error instanceof Error ? error.message : 'Unknown install error' + ); console.error( - '\n Failed to install globally. You may need sudo or fix npm permissions.' + `Check "${manager} --version" and your PATH. Retry: ${commands.install}` + ); + if (manager === 'pnpm') + console.error( + 'If the global bin directory is missing, run "pnpm setup" and restart your shell.' + ); + console.log( + `You can run without a global install: ${commands.run} ` ); return false; } @@ -949,10 +992,12 @@ export async function handleInitCommand( return; } + let installFailed = false; // Step 1: Install if (!options.skipInstall) { const ok = await stepInstall(); if (!ok) { + installFailed = true; console.log(` ${dim}Continuing with setup...${reset}\n`); } } @@ -974,10 +1019,22 @@ export async function handleInitCommand( // Step 5: Default web provider await stepDefaults(); - printNextSteps(skillCount, true); + printNextSteps( + skillCount, + true, + skillCount != null, + installFailed ? PACKAGE_MANAGERS[detectPackageManager()].run : 'firecrawl' + ); + if (installFailed) { + console.error( + 'Setup incomplete: global CLI installation failed. Other completed steps are preserved.' + ); + process.exitCode = 1; + } } async function runNonInteractive(options: InitOptions): Promise { + const failures: string[] = []; const steps: string[] = []; if (!options.skipAuth) steps.push('auth'); if (!options.skipInstall) steps.push('install'); @@ -1025,6 +1082,7 @@ async function runNonInteractive(options: InitOptions): Promise { '\nAuthentication failed:', error instanceof Error ? error.message : 'Unknown error' ); + failures.push('authentication'); console.log('You can authenticate later with: firecrawl login\n'); } } @@ -1032,21 +1090,11 @@ async function runNonInteractive(options: InitOptions): Promise { if (!options.skipInstall) { console.log(`${stepLabel()} Installing firecrawl-cli globally...`); - try { - execSync('npm install -g firecrawl-cli', { stdio: 'inherit' }); - console.log(`${green}✓${reset} CLI installed globally\n`); - warnIfCliNotOnPath(); - } catch { - console.error( - `\n${dim}Failed to install firecrawl-cli globally. You may need sudo or fix npm permissions.${reset}` - ); - console.log( - `${dim}Continuing — the CLI is already running via npx.${reset}\n` - ); - } + if (!installGlobalCli()) failures.push('global CLI installation'); } let skillCount: number | null = null; + let skillsInstalled = false; if (!options.skipSkills) { console.log( `${stepLabel()} Installing firecrawl skills for AI coding agents...` @@ -1054,15 +1102,29 @@ async function runNonInteractive(options: InitOptions): Promise { for (const selection of [INIT_CLI_SELECTION, INIT_WORKFLOW_SELECTION]) { try { const count = await installSkillRepoQuiet(selection, options); + skillsInstalled = true; if (count != null) skillCount = (skillCount ?? 0) + count; } catch { console.error( `\n${dim}Failed to install ${selection.label}. Retry with: ${selection.retryCommand}${reset}` ); - process.exit(1); + failures.push(selection.label); } } } - printNextSteps(skillCount); + printNextSteps( + skillCount, + false, + skillsInstalled, + failures.includes('global CLI installation') + ? PACKAGE_MANAGERS[detectPackageManager()].run + : 'firecrawl' + ); + if (failures.length > 0) { + console.error( + `Setup incomplete: ${failures.join(', ')} failed. Other completed steps are preserved.` + ); + process.exitCode = 1; + } } diff --git a/src/utils/package-manager.ts b/src/utils/package-manager.ts new file mode 100644 index 0000000000..b9977a809c --- /dev/null +++ b/src/utils/package-manager.ts @@ -0,0 +1,34 @@ +export type PackageManager = 'npm' | 'pnpm' | 'bun'; + +export function detectPackageManager( + env: NodeJS.ProcessEnv = process.env, + argv: string[] = process.argv +): PackageManager { + // Runner cache paths identify bunx/pnpm dlx even without lifecycle metadata. + const entry = (argv[1] ?? '').replaceAll('\\', '/'); + if (/bunx[-/]|\/\.bun\//.test(entry)) return 'bun'; + if (/\/pnpm\/dlx\/|\/\.cache\/pnpm\/dlx\//.test(entry)) return 'pnpm'; + const hint = `${env.npm_config_user_agent ?? ''} ${env.npm_execpath ?? ''}`; + if (/\bpnpm\b/i.test(hint)) return 'pnpm'; + if (/\bbun\b/i.test(hint)) return 'bun'; + if (process.versions.bun) return 'bun'; + return 'npm'; +} + +export const PACKAGE_MANAGERS = { + npm: { + install: 'npm install -g firecrawl-cli', + run: 'npx firecrawl-cli', + bin: 'npm prefix -g', + }, + pnpm: { + install: 'pnpm add -g firecrawl-cli', + run: 'pnpm dlx firecrawl-cli', + bin: 'pnpm bin -g', + }, + bun: { + install: 'bun add -g firecrawl-cli', + run: 'bunx firecrawl-cli', + bin: 'bun pm bin -g', + }, +} as const; From 4b12c446e895964dbcfd8d30cd9118e192a28f9c Mon Sep 17 00:00:00 2001 From: Developers Digest <124798203+developersdigest@users.noreply.github.com> Date: Tue, 22 Sep 2026 13:28:36 -0400 Subject: [PATCH 2/2] Preserve installer configuration and harden init success detection --- src/__tests__/commands/init.test.ts | 76 +++++++++++++++++++++ src/__tests__/utils/package-manager.test.ts | 18 ++++- src/commands/init.ts | 24 ++++--- src/utils/package-manager.ts | 9 +-- 4 files changed, 112 insertions(+), 15 deletions(-) diff --git a/src/__tests__/commands/init.test.ts b/src/__tests__/commands/init.test.ts index 37705ac204..8bd9dbbbf2 100644 --- a/src/__tests__/commands/init.test.ts +++ b/src/__tests__/commands/init.test.ts @@ -1,5 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { execSync } from 'child_process'; +import * as skillsNative from '../../commands/skills-native'; import { handleInitCommand } from '../../commands/init'; import { CLI_SKILLS, WORKFLOW_SKILLS } from '../../commands/skills-install'; @@ -19,6 +20,14 @@ vi.mock('child_process', () => ({ execSync: vi.fn(), })); +vi.mock('../../commands/skills-native', () => ({ + detectInstalledAgentNames: vi.fn(() => []), + hasNpx: vi.fn(() => true), + installSkillsNative: vi.fn(async () => { + throw new Error('Native install unavailable in this test'); + }), +})); + vi.mock('../../commands/setup', () => ({ installMcp: installMcpMock, })); @@ -89,6 +98,39 @@ describe('handleInitCommand', () => { ); }); + it('summarizes interactive skills success even when npx output has no count', async () => { + confirmMock + .mockResolvedValueOnce(true) + .mockResolvedValueOnce(false) + .mockResolvedValueOnce(false); + checkboxMock.mockResolvedValueOnce(['skills']); + vi.mocked(execSync).mockReturnValueOnce(Buffer.from('Done')); + await handleInitCommand({ skipInstall: true, skipAuth: true }); + expect(console.log).toHaveBeenCalledWith( + expect.stringContaining('Skills installed') + ); + }); + + it('summarizes native interactive installation independently of its count', async () => { + confirmMock + .mockResolvedValueOnce(true) + .mockResolvedValueOnce(false) + .mockResolvedValueOnce(false); + checkboxMock.mockResolvedValueOnce(['skills']); + vi.mocked(skillsNative.installSkillsNative).mockResolvedValueOnce({ + skillCount: 14, + linkedAgents: ['cursor'], + } as Awaited>); + await handleInitCommand({ + skipInstall: true, + skipAuth: true, + agent: 'cursor', + }); + expect(console.log).toHaveBeenCalledWith( + expect.stringContaining('14 skills') + ); + }); + it('routes interactive MCP setup through the hardened hosted installer', async () => { getApiKeyMock.mockReturnValue('fc-stored-key'); confirmMock @@ -172,6 +214,40 @@ describe('init global installation', () => { ); }); + it('preserves npm configuration during preflight, install, and bin discovery', async () => { + vi.stubEnv('npm_config_user_agent', 'npm/10.9.0'); + vi.stubEnv('npm_config_prefix', '/tmp/custom-prefix'); + vi.stubEnv('npm_config_registry', 'https://registry.example.com'); + vi.stubEnv('npm_config__auth', 'test-only-auth'); + vi.mocked(execSync).mockImplementation((command) => { + if ( + String(command).includes('firecrawl') && + !String(command).includes('install') + ) + throw new Error('not on PATH'); + return Buffer.from( + command === 'npm --version' ? '10.9.0' : '/tmp/custom-prefix' + ); + }); + await handleInitCommand({ all: true, skipAuth: true, skipSkills: true }); + for (const command of [ + 'npm --version', + 'npm install -g firecrawl-cli', + 'npm prefix -g', + ]) { + expect(execSync).toHaveBeenCalledWith( + command, + expect.objectContaining({ + env: expect.objectContaining({ + npm_config_prefix: '/tmp/custom-prefix', + npm_config_registry: 'https://registry.example.com', + npm_config__auth: 'test-only-auth', + }), + }) + ); + } + }); + it('rejects obsolete npm before installation and exits unsuccessfully', async () => { vi.stubEnv('npm_config_user_agent', 'npm/2.15.12'); vi.mocked(execSync).mockReturnValue(Buffer.from('2.15.12')); diff --git a/src/__tests__/utils/package-manager.test.ts b/src/__tests__/utils/package-manager.test.ts index e4d5b42204..a1587dda76 100644 --- a/src/__tests__/utils/package-manager.test.ts +++ b/src/__tests__/utils/package-manager.test.ts @@ -10,7 +10,7 @@ describe('package manager detection', () => { [{ npm_execpath: '/home/user/.bun/bin/bun' }, 'bun'], [{}, 'npm'], ] as const)('detects %j as %s', (env, expected) => { - expect(detectPackageManager(env, ['node', '/app/firecrawl'])).toBe( + expect(detectPackageManager(env, ['node', '/app/firecrawl'], {})).toBe( expected ); }); @@ -24,8 +24,16 @@ describe('package manager detection', () => { '/home/user/.cache/pnpm/dlx/123/node_modules/firecrawl-cli/dist/index.js', 'pnpm', ], + [ + '/home/user/.local/share/pnpm/global/5/node_modules/firecrawl-cli/dist/index.js', + 'pnpm', + ], + [ + 'C:\\Users\\user\\AppData\\Local\\pnpm\\global\\5\\node_modules\\firecrawl-cli\\dist\\index.js', + 'pnpm', + ], ])( - 'prefers runner cache path %s over inherited npm metadata', + 'prefers install or runner path %s over inherited npm metadata', (entry, expected) => { expect( detectPackageManager({ npm_config_user_agent: 'npm/2.15.12' }, [ @@ -35,4 +43,10 @@ describe('package manager detection', () => { ).toBe(expected); } ); + + it('detects a direct Bun invocation without package manager metadata', () => { + expect( + detectPackageManager({}, ['bun', '/app/dist/index.js'], { bun: '1.3.0' }) + ).toBe('bun'); + }); }); diff --git a/src/commands/init.ts b/src/commands/init.ts index 4fda5f4f7e..6fe9718135 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -369,7 +369,7 @@ function globalBinDir(manager: PackageManager): string | null { try { const prefix = execSync(PACKAGE_MANAGERS[manager].bin, { stdio: ['ignore', 'pipe', 'pipe'], - env: cleanNpmEnv(), + env: process.env, }) .toString() .trim(); @@ -453,7 +453,7 @@ function installGlobalCli(): boolean { try { const version = execSync(`${manager} --version`, { stdio: ['ignore', 'pipe', 'pipe'], - env: cleanNpmEnv(), + env: process.env, timeout: 10_000, }) .toString() @@ -469,7 +469,7 @@ function installGlobalCli(): boolean { `Unsupported ${manager} version ${version}. Update ${manager} and check your PATH before retrying.` ); } - execSync(commands.install, { stdio: 'inherit', env: cleanNpmEnv() }); + execSync(commands.install, { stdio: 'inherit', env: process.env }); console.log(`${green}✓${reset} CLI installed globally\n`); warnIfCliNotOnPath(manager); return true; @@ -562,7 +562,9 @@ export async function stepAuth(options: InitOptions): Promise { } } -async function stepIntegrations(options: InitOptions): Promise { +async function stepIntegrations( + options: InitOptions +): Promise<{ skillCount: number | null; skillsInstalled: boolean }> { const { checkbox, confirm } = await import('@inquirer/prompts'); const wantIntegrations = await confirm({ @@ -570,7 +572,7 @@ async function stepIntegrations(options: InitOptions): Promise { default: true, }); - if (!wantIntegrations) return null; + if (!wantIntegrations) return { skillCount: null, skillsInstalled: false }; const integrations = await checkbox({ message: 'Which integrations?', @@ -598,7 +600,7 @@ async function stepIntegrations(options: InitOptions): Promise { if (integrations.length === 0) { console.log(` ${dim}No integrations selected.${reset}\n`); - return null; + return { skillCount: null, skillsInstalled: false }; } // If skills/workflows are being installed, let the user route them to a @@ -614,6 +616,7 @@ async function stepIntegrations(options: InitOptions): Promise { : null; let totalSkills: number | null = null; + let skillsInstalled = false; for (const integration of integrations) { switch (integration) { case 'skills': { @@ -624,6 +627,7 @@ async function stepIntegrations(options: InitOptions): Promise { options, targetAgents ); + skillsInstalled = true; if (count != null) totalSkills = (totalSkills ?? 0) + count; } catch { console.error( @@ -653,6 +657,7 @@ async function stepIntegrations(options: InitOptions): Promise { options, targetAgents ); + skillsInstalled = true; if (count != null) totalSkills = (totalSkills ?? 0) + count; } catch { console.error( @@ -703,7 +708,7 @@ async function stepIntegrations(options: InitOptions): Promise { } } } - return totalSkills; + return { skillCount: totalSkills, skillsInstalled }; } /** @@ -1009,8 +1014,9 @@ export async function handleInitCommand( // Step 3: Integrations (skills, MCP, env) let skillCount: number | null = null; + let skillsInstalled = false; if (!options.skipSkills) { - skillCount = await stepIntegrations(options); + ({ skillCount, skillsInstalled } = await stepIntegrations(options)); } // Step 4: Template @@ -1022,7 +1028,7 @@ export async function handleInitCommand( printNextSteps( skillCount, true, - skillCount != null, + skillsInstalled, installFailed ? PACKAGE_MANAGERS[detectPackageManager()].run : 'firecrawl' ); if (installFailed) { diff --git a/src/utils/package-manager.ts b/src/utils/package-manager.ts index b9977a809c..0da9e56190 100644 --- a/src/utils/package-manager.ts +++ b/src/utils/package-manager.ts @@ -2,16 +2,17 @@ export type PackageManager = 'npm' | 'pnpm' | 'bun'; export function detectPackageManager( env: NodeJS.ProcessEnv = process.env, - argv: string[] = process.argv + argv: string[] = process.argv, + versions: Partial = process.versions ): PackageManager { - // Runner cache paths identify bunx/pnpm dlx even without lifecycle metadata. + // Install and runner paths identify the manager without lifecycle metadata. const entry = (argv[1] ?? '').replaceAll('\\', '/'); if (/bunx[-/]|\/\.bun\//.test(entry)) return 'bun'; - if (/\/pnpm\/dlx\/|\/\.cache\/pnpm\/dlx\//.test(entry)) return 'pnpm'; + if (/\/pnpm\/(dlx|global)\//.test(entry)) return 'pnpm'; const hint = `${env.npm_config_user_agent ?? ''} ${env.npm_execpath ?? ''}`; if (/\bpnpm\b/i.test(hint)) return 'pnpm'; if (/\bbun\b/i.test(hint)) return 'bun'; - if (process.versions.bun) return 'bun'; + if (versions.bun) return 'bun'; return 'npm'; }