diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_without_global_vp/assert-create.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_without_global_vp/assert-create.mjs new file mode 100644 index 0000000000..ce324ed058 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_without_global_vp/assert-create.mjs @@ -0,0 +1,61 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const [packageManager, mode] = process.argv.slice(2); +const env = { ...process.env }; +const pathKey = Object.keys(env).find((key) => key.toUpperCase() === 'PATH'); +// Keep system tools such as Git, but remove every directory that exposes vp. +const vpExecutables = ['vp', 'vp.exe', 'vp.cmd', 'vp.bat']; +const pathsWithoutVp = env[pathKey] + .split(path.delimiter) + .filter((dir) => !vpExecutables.some((bin) => existsSync(path.join(dir, bin)))); +env[pathKey] = [path.dirname(process.execPath), ...pathsWithoutVp].join(path.delimiter); +delete env.VP_CLI_BIN; +assert.equal(spawnSync('vp', ['--version'], { env }).error?.code, 'ENOENT'); + +if (mode === 'unrelated') { + const binDir = path.resolve('unrelated-bin'); + mkdirSync(binDir); + writeFileSync(path.join(binDir, 'vp'), '#!/bin/sh\necho unrelated-vp >&2\nexit 99\n', { mode: 0o755 }); + writeFileSync(path.join(binDir, 'vp.cmd'), '@echo unrelated-vp >&2\r\n@exit /b 99\r\n'); + env[pathKey] = `${binDir}${path.delimiter}${env[pathKey]}`; +} + +env.npm_config_user_agent = packageManager.replace('@', '/'); +const cli = fileURLToPath(import.meta.resolve('vite-plus/bin')); +const projectDir = 'projects with spaces/app'; +// Reuse the local bundled-template fixture, without an external template download. +const result = spawnSync( + process.execPath, + [ + cli, + 'create', + '@your-org:demo', + '--directory', + projectDir, + '--no-interactive', + '--no-agent', + '--no-editor', + '--git', + '--hooks', + '--verbose', + ], + { env, encoding: 'utf8', timeout: 210000 }, +); +const output = `${result.stdout}\n${result.stderr}`; +assert.equal(result.status, 0, output || result.error?.message); +assert.match(output, /Dependencies installed/); +assert.match(output, /Code formatted/); +assert.match(output, /Git hooks configured/); +assert.doesNotMatch(output, /unrelated-vp|Failed to install git hooks|Install failed|Format failed/); +assert.ok(existsSync(path.join(projectDir, 'node_modules/vite-plus/package.json'))); +assert.match(readFileSync(path.join(projectDir, '.git/config'), 'utf8'), /hooksPath/); +const lockfile = packageManager.startsWith('pnpm@') ? 'pnpm-lock.yaml' : 'yarn.lock'; +assert.ok(existsSync(path.join(projectDir, lockfile))); +console.log( + `Created a project with ${packageManager.split('@')[0]} and ${mode === 'missing' ? 'no' : 'an unrelated'} vp on PATH.`, +); +console.log('Dependencies installed, code formatted, and Git hooks configured.'); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_without_global_vp/mock-manifest.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_without_global_vp/mock-manifest.json new file mode 100644 index 0000000000..67f065589e --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_without_global_vp/mock-manifest.json @@ -0,0 +1,34 @@ +{ + "@your-org/create": { + "name": "@your-org/create", + "dist-tags": { + "latest": "1.0.0" + }, + "versions": { + "1.0.0": { + "version": "1.0.0", + "dist": { + "tarball": "{REGISTRY}/@your-org/create/-/create-1.0.0.tgz", + "integrity": "sha512-swwZCXG8RDBJNTmiw3zCOBCQx9bCg7wHqoW37aV7BgGYfBUU7n7c+efbSCg0+FpdLhgeUjEMP5IAO+8exmeFKg==" + }, + "createConfig": { + "templates": [ + { + "name": "demo", + "description": "Bundled demo template", + "template": "./templates/demo" + } + ] + } + } + } + }, + "pnpm/latest": { + "name": "pnpm", + "version": "12.11.2" + }, + "@yarnpkg/cli-dist/latest": { + "name": "@yarnpkg/cli-dist", + "version": "4.18.1" + } +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_without_global_vp/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_without_global_vp/snapshots.toml new file mode 100644 index 0000000000..95771702ba --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_without_global_vp/snapshots.toml @@ -0,0 +1,26 @@ +[[case]] +name = "pnpm_without_global_vp" +vp = "local" +local-registry = true +unset-env = ["VP_SKIP_INSTALL", "CI"] +steps = [ + { argv = ["node", "assert-create.mjs", "pnpm@12.11.2", "missing"], timeout = 240000 }, +] + +[[case]] +name = "yarn_without_global_vp" +vp = "local" +local-registry = true +unset-env = ["VP_SKIP_INSTALL", "CI"] +steps = [ + { argv = ["node", "assert-create.mjs", "yarn@4.18.1", "missing"], timeout = 240000 }, +] + +[[case]] +name = "ignores_unrelated_global_vp" +vp = "local" +local-registry = true +unset-env = ["VP_SKIP_INSTALL", "CI"] +steps = [ + { argv = ["node", "assert-create.mjs", "pnpm@12.11.2", "unrelated"], timeout = 240000 }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_without_global_vp/snapshots/ignores_unrelated_global_vp.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_without_global_vp/snapshots/ignores_unrelated_global_vp.md new file mode 100644 index 0000000000..e714b295a6 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_without_global_vp/snapshots/ignores_unrelated_global_vp.md @@ -0,0 +1,8 @@ +# ignores_unrelated_global_vp + +## `node assert-create.mjs pnpm@12.11.2 unrelated` + +``` +Created a project with pnpm and an unrelated vp on PATH. +Dependencies installed, code formatted, and Git hooks configured. +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_without_global_vp/snapshots/pnpm_without_global_vp.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_without_global_vp/snapshots/pnpm_without_global_vp.md new file mode 100644 index 0000000000..4cbd13eb54 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_without_global_vp/snapshots/pnpm_without_global_vp.md @@ -0,0 +1,8 @@ +# pnpm_without_global_vp + +## `node assert-create.mjs pnpm@12.11.2 missing` + +``` +Created a project with pnpm and no vp on PATH. +Dependencies installed, code formatted, and Git hooks configured. +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_without_global_vp/snapshots/yarn_without_global_vp.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_without_global_vp/snapshots/yarn_without_global_vp.md new file mode 100644 index 0000000000..4024e48258 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_without_global_vp/snapshots/yarn_without_global_vp.md @@ -0,0 +1,8 @@ +# yarn_without_global_vp + +## `node assert-create.mjs yarn@4.18.1 missing` + +``` +Created a project with yarn and no vp on PATH. +Dependencies installed, code formatted, and Git hooks configured. +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_without_global_vp/tarballs/create-1.0.0.tgz b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_without_global_vp/tarballs/create-1.0.0.tgz new file mode 100644 index 0000000000..b73b8ed8cd Binary files /dev/null and b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_without_global_vp/tarballs/create-1.0.0.tgz differ diff --git a/packages/cli/src/migration/__tests__/prettier.spec.ts b/packages/cli/src/migration/__tests__/prettier.spec.ts index c25167191a..408d49b55e 100644 --- a/packages/cli/src/migration/__tests__/prettier.spec.ts +++ b/packages/cli/src/migration/__tests__/prettier.spec.ts @@ -7,7 +7,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; vi.mock('@voidzero-dev/vite-plus-prompts', () => ({ log: { info: vi.fn(), success: vi.fn(), warn: vi.fn() }, })); -vi.mock('../../utils/command.ts', () => ({ +vi.mock(import('../../utils/command.ts'), async (importOriginal) => ({ + ...(await importOriginal()), runCommandSilently: vi.fn(), })); @@ -27,11 +28,13 @@ describe('Prettier migration', () => { }; beforeEach(() => { + vi.stubEnv('VP_CLI_BIN', undefined); projectPath = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-test-prettier-')); fs.writeFileSync(path.join(projectPath, 'package.json'), JSON.stringify(packageJson)); }); afterEach(() => { + vi.unstubAllEnvs(); fs.rmSync(projectPath, { recursive: true, force: true }); mockRunCommandSilently.mockReset(); }); @@ -56,7 +59,10 @@ describe('Prettier migration', () => { ).resolves.toBe('skipped'); expect(mockRunCommandSilently).toHaveBeenCalledWith( - expect.objectContaining({ args: ['fmt', '--migrate=prettier'], cwd: projectPath }), + expect.objectContaining({ + args: [expect.any(String), 'fmt', '--migrate=prettier'], + cwd: projectPath, + }), ); expect(fs.readFileSync(path.join(projectPath, configFile), 'utf8')).toBe(config); expect(fs.readFileSync(path.join(projectPath, 'package.json'), 'utf8')).toBe( diff --git a/packages/cli/src/migration/__tests__/tsup.spec.ts b/packages/cli/src/migration/__tests__/tsup.spec.ts index 7935beb699..b824b0f7d1 100644 --- a/packages/cli/src/migration/__tests__/tsup.spec.ts +++ b/packages/cli/src/migration/__tests__/tsup.spec.ts @@ -24,7 +24,8 @@ vi.mock('@voidzero-dev/vite-plus-prompts', () => ({ }, })); -vi.mock('../../utils/command.ts', () => ({ +vi.mock(import('../../utils/command.ts'), async (importOriginal) => ({ + ...(await importOriginal()), runCommandSilently: vi.fn(), })); vi.mock('../../utils/prompts.ts', () => ({ @@ -62,6 +63,7 @@ describe('tsup migration', () => { let projectPath: string; beforeEach(() => { + vi.stubEnv('VP_CLI_BIN', undefined); projectPath = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-test-tsup-')); fs.writeFileSync( path.join(projectPath, 'package.json'), @@ -80,6 +82,7 @@ describe('tsup migration', () => { }); afterEach(() => { + vi.unstubAllEnvs(); fs.rmSync(projectPath, { recursive: true, force: true }); mockRunCommandSilently.mockReset(); mockConfirm.mockReset(); @@ -96,7 +99,16 @@ describe('tsup migration', () => { expect(mockRunCommandSilently).toHaveBeenCalledWith( expect.objectContaining({ - args: ['dlx', 'tsdown-migrate@0.99.0', '--yes', '--package-manager', 'npm', '--no-install'], + command: process.execPath, + args: [ + expect.any(String), + 'dlx', + 'tsdown-migrate@0.99.0', + '--yes', + '--package-manager', + 'npm', + '--no-install', + ], }), ); }); diff --git a/packages/cli/src/migration/migrator/eslint.ts b/packages/cli/src/migration/migrator/eslint.ts index 992a6e7e5f..bf9231d0e1 100644 --- a/packages/cli/src/migration/migrator/eslint.ts +++ b/packages/cli/src/migration/migrator/eslint.ts @@ -9,7 +9,7 @@ import { compareReversed, isLessThanOrEqual, normalize } from 'verkit'; import { ResolverFactory, rewriteEslint } from '../../../binding/index.js'; import { type WorkspacePackage } from '../../types/index.ts'; -import { runCommandSilently } from '../../utils/command.ts'; +import { getVpCommand, runCommandSilently } from '../../utils/command.ts'; import { editJsonFile, isJsonFile, readJsonFile } from '../../utils/json.ts'; import { fetchNpmResource, getNpmRegistry } from '../../utils/npm-config.ts'; import { displayRelative } from '../../utils/path.ts'; @@ -180,7 +180,6 @@ export async function resolveOxlintMigrateVersion(oxlintVersion: string): Promis * Returns true on success, false on failure (spawn error or non-zero exit). */ async function runOxlintMigrateStep( - vpBin: string, cwd: string, migratePackage: string, args: string[], @@ -190,8 +189,7 @@ async function runOxlintMigrateStep( ): Promise<{ ok: boolean; skippedRules?: string }> { try { const result = await runCommandSilently({ - command: vpBin, - args: ['dlx', migratePackage, ...args], + ...getVpCommand(['dlx', migratePackage, ...args]), cwd, envs: process.env, }); @@ -219,7 +217,6 @@ export async function migrateEslintToOxlint( packages?: WorkspacePackage[], options?: { silent?: boolean; report?: MigrationReport }, ): Promise { - const vpBin = process.env.VP_CLI_BIN ?? 'vp'; const spinner = options?.silent ? getSilentSpinner() : getSpinner(interactive); // Steps 1-2: Only run @oxlint/migrate if there's an eslint config at root @@ -240,7 +237,6 @@ export async function migrateEslintToOxlint( // Step 1: Generate .oxlintrc.json from ESLint config spinner.start('Migrating ESLint config to Oxlint...'); const migrateResult = await runOxlintMigrateStep( - vpBin, projectPath, migratePackage, migrateArgs, @@ -259,7 +255,6 @@ export async function migrateEslintToOxlint( // Step 2: Replace eslint-disable comments with oxlint-disable spinner.start('Replacing ESLint comments with Oxlint equivalents...'); const replaceResult = await runOxlintMigrateStep( - vpBin, projectPath, migratePackage, ['--replace-eslint-comments'], diff --git a/packages/cli/src/migration/migrator/git-hooks.ts b/packages/cli/src/migration/migrator/git-hooks.ts index d0de296d52..7273b40d4c 100644 --- a/packages/cli/src/migration/migrator/git-hooks.ts +++ b/packages/cli/src/migration/migrator/git-hooks.ts @@ -17,6 +17,7 @@ import { SUPPORTED_GIT_HOOK_NAMES, } from '../../config/hooks.ts'; import type { PackageManager, WorkspacePackage } from '../../types/index.ts'; +import { getVpCommand } from '../../utils/command.ts'; import { documentationUrl } from '../../utils/documentation.ts'; import { findGitRoot } from '../../utils/git.ts'; import { editJsonFile, isJsonFile, readJsonFile } from '../../utils/json.ts'; @@ -460,10 +461,9 @@ export function installGitHooks( return true; } - const vpBin = process.env.VP_CLI_BIN ?? 'vp'; - // Install git hooks via vp config (--no-agent to skip agent setup, handled by migration) - const configResult = spawn.sync(vpBin, ['config', '--no-agent'], { + const { command, args } = getVpCommand(['config', '--no-agent']); + const configResult = spawn.sync(command, args, { cwd: projectPath, stdio: 'pipe', }); diff --git a/packages/cli/src/migration/migrator/prettier.ts b/packages/cli/src/migration/migrator/prettier.ts index 15338951b2..f837621375 100644 --- a/packages/cli/src/migration/migrator/prettier.ts +++ b/packages/cli/src/migration/migrator/prettier.ts @@ -6,7 +6,7 @@ import * as prompts from '@voidzero-dev/vite-plus-prompts'; import { rewritePrettier } from '../../../binding/index.js'; import { type WorkspacePackage } from '../../types/index.ts'; -import { runCommandSilently } from '../../utils/command.ts'; +import { getVpCommand, runCommandSilently } from '../../utils/command.ts'; import { editJsonFile, readJsonFile } from '../../utils/json.ts'; import { displayRelative } from '../../utils/path.ts'; import { cancelAndExit } from '../../utils/prompts.ts'; @@ -61,7 +61,6 @@ export function detectPrettierProject( * Unsupported config formats are skipped; other errors still fail migration. */ async function runPrettierMigrateStep( - vpBin: string, cwd: string, spinner: ReturnType, failMessage: string, @@ -69,8 +68,7 @@ async function runPrettierMigrateStep( ): Promise<'migrated' | 'skipped' | 'failed'> { try { const result = await runCommandSilently({ - command: vpBin, - args: ['fmt', '--migrate=prettier'], + ...getVpCommand(['fmt', '--migrate=prettier']), cwd, envs: process.env, }); @@ -109,7 +107,6 @@ export async function migratePrettierToOxfmt( packages?: WorkspacePackage[], options?: { silent?: boolean; report?: MigrationReport }, ): Promise<'migrated' | 'skipped' | 'failed'> { - const vpBin = process.env.VP_CLI_BIN ?? 'vp'; const spinner = options?.silent ? getSilentSpinner() : getSpinner(interactive); // Step 1: Generate .oxfmtrc.json from Prettier config @@ -133,7 +130,6 @@ export async function migratePrettierToOxfmt( try { spinner.start('Migrating Prettier config to Oxfmt...'); const result = await runPrettierMigrateStep( - vpBin, projectPath, spinner, 'Prettier migration failed', diff --git a/packages/cli/src/migration/migrator/tsup.ts b/packages/cli/src/migration/migrator/tsup.ts index 7eff1306e4..d9e5846fe9 100644 --- a/packages/cli/src/migration/migrator/tsup.ts +++ b/packages/cli/src/migration/migrator/tsup.ts @@ -6,7 +6,7 @@ import * as prompts from '@voidzero-dev/vite-plus-prompts'; import corePkg from 'vite/package.json' with { type: 'json' }; import { PackageManager, type WorkspacePackage } from '../../types/index.ts'; -import { runCommandSilently } from '../../utils/command.ts'; +import { getVpCommand, runCommandSilently } from '../../utils/command.ts'; import { TSDOWN_MIGRATION_SKILL_URL } from '../../utils/constants.ts'; import { editJsonFile, readJsonFile } from '../../utils/json.ts'; import { displayRelative } from '../../utils/path.ts'; @@ -372,21 +372,19 @@ function extractTsdownMigrationWarnings(output: Buffer): string[] { /** Run `vp dlx tsdown-migrate` in `cwd` with graceful error handling. */ async function runTsdownMigrateStep( - vpBin: string, cwd: string, packageManager: PackageManager, ): Promise<{ ok: boolean; warnings: string[] }> { try { const result = await runCommandSilently({ - command: vpBin, - args: [ + ...getVpCommand([ 'dlx', `tsdown-migrate@${corePkg.bundledVersions.tsdown}`, '--yes', '--package-manager', packageManager, '--no-install', - ], + ]), cwd, envs: process.env, }); @@ -417,7 +415,6 @@ export async function migrateTsupToTsdown( packages?: WorkspacePackage[], options?: { silent?: boolean; report?: MigrationReport }, ): Promise { - const vpBin = process.env.VP_CLI_BIN ?? 'vp'; const spinner = options?.silent ? getSilentSpinner() : getSpinner(interactive); // A tsup config isn't necessarily workspace-wide the way an ESLint flat @@ -491,7 +488,7 @@ export async function migrateTsupToTsdown( for (const target of tsupTargets) { const targetLabel = target === rootPath ? 'the project root' : displayRelative(target, projectPath); - const migrateResult = await runTsdownMigrateStep(vpBin, target, packageManager); + const migrateResult = await runTsdownMigrateStep(target, packageManager); if (!migrateResult.ok) { spinner.stop(); const restoreFailures = restoreTsupMigrationTargets(snapshots); diff --git a/packages/cli/src/utils/__tests__/run-vite-install.spec.ts b/packages/cli/src/utils/__tests__/run-vite-install.spec.ts index 087ade72c9..65b91fc574 100644 --- a/packages/cli/src/utils/__tests__/run-vite-install.spec.ts +++ b/packages/cli/src/utils/__tests__/run-vite-install.spec.ts @@ -1,8 +1,9 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { PackageManager } from '../../types/index.ts'; -vi.mock('../command.ts', () => ({ +vi.mock(import('../command.ts'), async (importOriginal) => ({ + ...(await importOriginal()), runCommandSilently: vi.fn(), })); @@ -18,7 +19,19 @@ function installResult(exitCode: number, stdout = '', stderr = '') { describe('runViteInstall with detectIgnoredBuilds', () => { beforeEach(() => { mockRun.mockReset(); - delete process.env.VP_SKIP_INSTALL; + vi.stubEnv('VP_SKIP_INSTALL', undefined); + vi.stubEnv('VP_CLI_BIN', undefined); + }); + + afterEach(() => vi.unstubAllEnvs()); + + it('preserves an explicit VP_CLI_BIN override and its arguments', async () => { + vi.stubEnv('VP_CLI_BIN', '/custom/vp'); + mockRun.mockResolvedValue(installResult(0)); + await runViteInstall('/project', false, ['--offline'], { silent: true }); + expect(mockRun).toHaveBeenCalledWith( + expect.objectContaining({ command: '/custom/vp', args: ['install', '--offline'] }), + ); }); it('treats pnpm >= 11 ERR_PNPM_IGNORED_BUILDS exit-1 as a completed install', async () => { @@ -38,7 +51,9 @@ describe('runViteInstall with detectIgnoredBuilds', () => { expect(summary.pendingBuilds).toEqual(['better-sqlite3', 'esbuild']); // Detection mode must NOT add --ignore-scripts (that would hide the builds // from `approve-builds` afterwards). - expect(mockRun).toHaveBeenCalledWith(expect.objectContaining({ args: ['install'] })); + expect(mockRun).toHaveBeenCalledWith( + expect.objectContaining({ command: process.execPath, args: [expect.any(String), 'install'] }), + ); }); it('still reports a genuine non-zero exit (no ignored-builds error) as failed', async () => { @@ -74,7 +89,7 @@ describe('runViteInstall with detectIgnoredBuilds', () => { expect(summary.status).toBe('installed'); expect(summary.pendingBuilds).toBeUndefined(); expect(mockRun).toHaveBeenCalledWith( - expect.objectContaining({ args: ['install', '--ignore-scripts'] }), + expect.objectContaining({ args: [expect.any(String), 'install', '--ignore-scripts'] }), ); }); }); diff --git a/packages/cli/src/utils/approve-builds.ts b/packages/cli/src/utils/approve-builds.ts index 351c1a52b9..6d9b18c37d 100644 --- a/packages/cli/src/utils/approve-builds.ts +++ b/packages/cli/src/utils/approve-builds.ts @@ -4,7 +4,7 @@ import { stripVTControlCharacters } from 'node:util'; import * as prompts from '@voidzero-dev/vite-plus-prompts'; import { PackageManager } from '../types/index.ts'; -import { runCommandSilently } from './command.ts'; +import { getVpCommand, runCommandSilently } from './command.ts'; import { readJsonFile, writeJsonFile } from './json.ts'; import { getSilentSpinner, getSpinner } from './spinner.ts'; import { accent } from './terminal.ts'; @@ -360,8 +360,7 @@ export async function detectGatedBuilds( } if (packageManager === PackageManager.bun) { const { exitCode, stdout, stderr } = await runCommandSilently({ - command: process.env.VP_CLI_BIN ?? 'vp', - args: ['exec', 'bun', 'pm', 'untrusted'], + ...getVpCommand(['exec', 'bun', 'pm', 'untrusted']), cwd: installCwd, envs: process.env, }); @@ -425,8 +424,7 @@ async function runBuildAndReport( const spinner = silent ? getSilentSpinner() : getSpinner(interactive); spinner.start(`Building ${packages.join(', ')}...`); const { exitCode, stdout, stderr } = await runCommandSilently({ - command: process.env.VP_CLI_BIN ?? 'vp', - args, + ...getVpCommand(args), cwd, envs: extraEnv ? { ...process.env, ...extraEnv } : process.env, }); @@ -462,8 +460,7 @@ async function approveNpmScripts( ): Promise { const approveCommand = `vp pm approve-builds ${packages.join(' ')}`; const { exitCode, stdout, stderr } = await runCommandSilently({ - command: process.env.VP_CLI_BIN ?? 'vp', - args: ['pm', 'approve-builds', ...packages], + ...getVpCommand(['pm', 'approve-builds', ...packages]), cwd: installCwd, envs: process.env, }); diff --git a/packages/cli/src/utils/command.ts b/packages/cli/src/utils/command.ts index 43d290d999..1a5543f0f5 100644 --- a/packages/cli/src/utils/command.ts +++ b/packages/cli/src/utils/command.ts @@ -1,4 +1,5 @@ import { constants } from 'node:os'; +import { fileURLToPath } from 'node:url'; import spawn from 'cross-spawn'; @@ -23,6 +24,20 @@ export interface RunCommandResult extends ExecutionResult { stderr: Buffer; } +/** Run this CLI version even when a package-manager launcher does not expose `vp` on PATH. */ +export function getVpCommand( + args: string[], + command = process.env.VP_CLI_BIN, +): Pick { + if (command !== undefined) { + return { command, args }; + } + return { + command: process.execPath, + args: [fileURLToPath(import.meta.resolve('vite-plus/bin')), ...args], + }; +} + export function exitCodeFromClose(code: number | null, signal: NodeJS.Signals | null) { if (code !== null) { return code; diff --git a/packages/cli/src/utils/prompts.ts b/packages/cli/src/utils/prompts.ts index 2350798a36..95a9f5900d 100644 --- a/packages/cli/src/utils/prompts.ts +++ b/packages/cli/src/utils/prompts.ts @@ -4,7 +4,7 @@ import { coerce, isGreaterThanOrEqual } from 'verkit'; import { downloadPackageManager as downloadPackageManagerBinding } from '../../binding/index.js'; import { PackageManager } from '../types/index.ts'; import { isPnpmIgnoredBuildsError, parseInstallGatedBuilds } from './approve-builds.ts'; -import { runCommandSilently } from './command.ts'; +import { getVpCommand, runCommandSilently } from './command.ts'; import { reconcilePreviewBridgeRegistry } from './preview-registry.ts'; import { getSilentSpinner, getSpinner } from './spinner.ts'; import { accent } from './terminal.ts'; @@ -145,8 +145,7 @@ export async function runViteInstall( const startTime = Date.now(); spinner.start(`Installing dependencies...`); const { exitCode, stderr, stdout } = await runCommandSilently({ - command: process.env.VP_CLI_BIN ?? 'vp', - args: ['install', ...installArgs], + ...getVpCommand(['install', ...installArgs]), cwd, envs: process.env, }); @@ -191,8 +190,7 @@ export async function runViteFmt( spinner.start(`Formatting code...`); const { exitCode, stderr, stdout } = await runCommandSilently({ - command: options?.command ?? process.env.VP_CLI_BIN ?? 'vp', - args: [...(options?.commandArgs ?? []), 'fmt', ...(paths ?? [])], + ...getVpCommand([...(options?.commandArgs ?? []), 'fmt', ...(paths ?? [])], options?.command), cwd, envs: process.env, }); diff --git a/packages/cli/src/utils/tsconfig.ts b/packages/cli/src/utils/tsconfig.ts index d952120a3d..6da34c4b73 100644 --- a/packages/cli/src/utils/tsconfig.ts +++ b/packages/cli/src/utils/tsconfig.ts @@ -6,7 +6,7 @@ import * as prompts from '@voidzero-dev/vite-plus-prompts'; import { applyEdits, modify, parse as parseJsonc } from 'jsonc-parser'; import vitestV5EntryPoints from '../vitest-v5-entry-points.json' with { type: 'json' }; -import { runCommandSilently } from './command.ts'; +import { getVpCommand, runCommandSilently } from './command.ts'; import { BASEURL_TSCONFIG_FIX_PACKAGE, createBaseUrlTsconfigFixArgs } from './constants.ts'; import { cancelAndExit } from './prompts.ts'; @@ -103,8 +103,7 @@ export async function fixBaseUrlInTsconfig( prompts.log.info(`Running vp dlx ${BASEURL_TSCONFIG_FIX_PACKAGE} ${fixArgs.join(' ')}`); } const result = await runCommandSilently({ - command: process.env.VP_CLI_BIN ?? 'vp', - args: ['dlx', BASEURL_TSCONFIG_FIX_PACKAGE, ...fixArgs], + ...getVpCommand(['dlx', BASEURL_TSCONFIG_FIX_PACKAGE, ...fixArgs]), cwd: projectPath, envs: process.env, });