From cd9e8575f41a1003e5f2d2f1b54c1661c6251c0a Mon Sep 17 00:00:00 2001 From: neverland Date: Fri, 21 Aug 2026 11:08:46 +0800 Subject: [PATCH] feat: support package manager-specific create commands --- src/index.ts | 32 +++---------------- src/package-manager.ts | 59 ++++++++++++++++++++++++++++++++++++ test/package-manager.test.ts | 46 ++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 27 deletions(-) create mode 100644 src/package-manager.ts create mode 100644 test/package-manager.test.ts diff --git a/src/index.ts b/src/index.ts index 4bd528a..4aee23b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,6 +19,10 @@ import deepmerge from 'deepmerge'; import minimist from 'minimist'; import { color, logger } from 'rslog'; import { x, xSync } from 'tinyexec'; +import { + getAgentCreateCommand, + replaceCreateCommand, +} from './package-manager.js'; import { isNpmTemplate, resolveCustomTemplate } from './template-manager.js'; const __filename = fileURLToPath(import.meta.url); @@ -82,12 +86,6 @@ function pkgFromUserAgent(userAgent: string | undefined) { }; } -function getAgentCreateCommand(name: string, packageManager: string) { - return packageManager === 'nub' - ? `nub create ${name}@latest` - : `npx -y create-${name}`; -} - const PACKAGE_MANAGER_FILES = [ { file: 'pnpm-workspace.yaml', packageManager: 'pnpm' }, ]; @@ -504,27 +502,7 @@ async function runCommand( cwd: string, packageManager: string, ) { - // Replace `npm create` with the equivalent command for the detected package manager - if (command.startsWith('npm create ')) { - const createReplacements: Record = { - bun: 'bun create ', - nub: 'nub create ', - pnpm: 'pnpm create ', - yarn: 'yarn create ', - deno: 'deno run -A npm:create-', - }; - const replacement = createReplacements[packageManager]; - if (replacement) { - command = command - .replace('npm create ', replacement) - // other package managers don't need the extra `--` - .replace(' -- --', ' --'); - } - // Yarn v1 does not support `@latest` tag - if (packageManager === 'yarn') { - command = command.replace('@latest', ''); - } - } + command = replaceCreateCommand(command, packageManager); const result = await x(command, [], { nodeOptions: { diff --git a/src/package-manager.ts b/src/package-manager.ts new file mode 100644 index 0000000..c4067a7 --- /dev/null +++ b/src/package-manager.ts @@ -0,0 +1,59 @@ +const CREATE_COMMAND_REPLACEMENTS: Record = { + bun: 'bun create ', + nub: 'nub create ', + pnpm: 'pnpm create ', + yarn: 'yarn create ', + deno: 'deno run -A npm:create-', +}; + +function addNpmYesFlag(command: string) { + const separatorIndex = command.indexOf(' -- '); + const npmCommand = + separatorIndex === -1 ? command : command.slice(0, separatorIndex); + + if (/(?:^|\s)(?:-y|--yes(?:=\S+)?)(?=\s|$)/.test(npmCommand)) { + return command; + } + + return separatorIndex === -1 + ? `${command} -y` + : `${npmCommand} -y${command.slice(separatorIndex)}`; +} + +export function replaceCreateCommand(command: string, packageManager: string) { + if (!command.startsWith('npm create ')) { + return command; + } + + if (packageManager === 'npm') { + return addNpmYesFlag(command); + } + + const replacement = CREATE_COMMAND_REPLACEMENTS[packageManager]; + if (!replacement) { + return command; + } + + const replacedCommand = command + .replace('npm create ', replacement) + // Other package managers don't need the extra `--`. + .replace(' -- --', ' --'); + + // Yarn v1 does not support the `@latest` tag. + return packageManager === 'yarn' + ? replacedCommand.replace('@latest', '') + : replacedCommand; +} + +export function getAgentCreateCommand(name: string, packageManager: string) { + if (packageManager === 'npm') { + return `npx -y create-${name}@latest`; + } + + const npmCreateCommand = `npm create ${name}@latest`; + const createCommand = replaceCreateCommand(npmCreateCommand, packageManager); + + return createCommand === npmCreateCommand + ? `npx -y create-${name}@latest` + : createCommand; +} diff --git a/test/package-manager.test.ts b/test/package-manager.test.ts new file mode 100644 index 0000000..0caeeec --- /dev/null +++ b/test/package-manager.test.ts @@ -0,0 +1,46 @@ +import { expect, test } from 'rstack/test'; +import { + getAgentCreateCommand, + replaceCreateCommand, +} from '../src/package-manager'; + +test.each([ + ['npm', 'npx -y create-test@latest'], + ['pnpm', 'pnpm create test@latest'], + ['yarn', 'yarn create test'], + ['bun', 'bun create test@latest'], + ['deno', 'deno run -A npm:create-test@latest'], + ['unknown', 'npx -y create-test@latest'], +])('should get the agent create command for %s', (packageManager, expected) => { + expect(getAgentCreateCommand('test', packageManager)).toBe(expected); +}); + +test.each([ + ['npm', 'npm create test@latest -y -- --template vanilla'], + ['pnpm', 'pnpm create test@latest --template vanilla'], + ['yarn', 'yarn create test --template vanilla'], + ['bun', 'bun create test@latest --template vanilla'], + ['deno', 'deno run -A npm:create-test@latest --template vanilla'], + ['unknown', 'npm create test@latest -- --template vanilla'], +])('should replace the create command for %s', (packageManager, expected) => { + expect( + replaceCreateCommand( + 'npm create test@latest -- --template vanilla', + packageManager, + ), + ).toBe(expected); +}); + +test.each([ + 'npm create test@latest -y -- --template vanilla', + 'npm create test@latest --yes -- --template vanilla', + 'npm create test@latest --yes=false -- --template vanilla', +])('should preserve an existing npm yes flag in %s', (command) => { + expect(replaceCreateCommand(command, 'npm')).toBe(command); +}); + +test('should distinguish an npm flag from a scaffolder argument', () => { + expect(replaceCreateCommand('npm create test@latest -- --yes', 'npm')).toBe( + 'npm create test@latest -y -- --yes', + ); +});