11import { execa , type ExecaError } from 'execa'
22import { logger } from './logger.js'
3+ import fs from 'fs-extra'
4+ import path from 'path'
35
46export type PackageManager = 'pnpm' | 'npm' | 'yarn'
57
68/**
7- * Detect which package manager is available
9+ * Validate if a string is a supported package manager
810 */
9- export async function detectPackageManager ( ) : Promise < PackageManager | null > {
10- const managers : PackageManager [ ] = [ 'pnpm' , 'npm' , 'yarn' ]
11+ function isValidPackageManager ( manager : string ) : manager is PackageManager {
12+ return manager === 'pnpm' || manager === 'npm' || manager === 'yarn'
13+ }
14+
15+ /**
16+ * Detect which package manager to use for a project
17+ * Checks in order:
18+ * 1. packageManager field in package.json (Node.js standard)
19+ * 2. Lock files (pnpm-lock.yaml, package-lock.json, yarn.lock)
20+ * 3. Installed package managers (system-wide check)
21+ * 4. Defaults to npm if all detection fails
22+ *
23+ * @param cwd Working directory to detect package manager in (defaults to process.cwd())
24+ * @returns The detected package manager, or 'npm' as default
25+ */
26+ export async function detectPackageManager ( cwd : string = process . cwd ( ) ) : Promise < PackageManager > {
27+ // 1. Check packageManager field in package.json
28+ try {
29+ const packageJsonPath = path . join ( cwd , 'package.json' )
30+ if ( await fs . pathExists ( packageJsonPath ) ) {
31+ const packageJsonContent = await fs . readFile ( packageJsonPath , 'utf-8' )
32+ const packageJson = JSON . parse ( packageJsonContent )
33+
34+ if ( packageJson . packageManager ) {
35+ // Parse "[email protected] " or "[email protected] +sha512..." -> "pnpm" 36+ const manager = packageJson . packageManager . split ( '@' ) [ 0 ]
37+ if ( isValidPackageManager ( manager ) ) {
38+ logger . debug ( `Detected package manager from package.json: ${ manager } ` )
39+ return manager
40+ }
41+ }
42+ }
43+ } catch ( error ) {
44+ // If package.json doesn't exist, is malformed, or unreadable, continue to next detection method
45+ logger . debug ( `Could not read packageManager from package.json: ${ error instanceof Error ? error . message : 'Unknown error' } ` )
46+ }
1147
48+ // 2. Check lock files (priority: pnpm > npm > yarn)
49+ const lockFiles : Array < { file : string ; manager : PackageManager } > = [
50+ { file : 'pnpm-lock.yaml' , manager : 'pnpm' } ,
51+ { file : 'package-lock.json' , manager : 'npm' } ,
52+ { file : 'yarn.lock' , manager : 'yarn' } ,
53+ ]
54+
55+ for ( const { file, manager } of lockFiles ) {
56+ if ( await fs . pathExists ( path . join ( cwd , file ) ) ) {
57+ logger . debug ( `Detected package manager from lock file ${ file } : ${ manager } ` )
58+ return manager
59+ }
60+ }
61+
62+ // 3. Check installed package managers (original behavior)
63+ const managers : PackageManager [ ] = [ 'pnpm' , 'npm' , 'yarn' ]
1264 for ( const manager of managers ) {
1365 try {
1466 await execa ( manager , [ '--version' ] )
67+ logger . debug ( `Detected installed package manager: ${ manager } ` )
1568 return manager
1669 } catch {
1770 // Continue to next manager
1871 }
1972 }
2073
21- return null
74+ // 4. Default to npm (always available in Node.js environments)
75+ logger . debug ( 'No package manager detected, defaulting to npm' )
76+ return 'npm'
2277}
2378
2479/**
@@ -31,11 +86,7 @@ export async function installDependencies(
3186 cwd : string ,
3287 frozen : boolean = true
3388) : Promise < void > {
34- const packageManager = await detectPackageManager ( )
35-
36- if ( ! packageManager ) {
37- throw new Error ( 'No package manager found (pnpm, npm, or yarn)' )
38- }
89+ const packageManager = await detectPackageManager ( cwd )
3990
4091 logger . info ( `Installing dependencies with ${ packageManager } ...` )
4192
@@ -83,11 +134,7 @@ export async function runScript(
83134 cwd : string ,
84135 args : string [ ] = [ ]
85136) : Promise < void > {
86- const packageManager = await detectPackageManager ( )
87-
88- if ( ! packageManager ) {
89- throw new Error ( 'No package manager found (pnpm, npm, or yarn)' )
90- }
137+ const packageManager = await detectPackageManager ( cwd )
91138
92139 const command = packageManager === 'npm' ? [ 'run' , scriptName ] : [ scriptName ]
93140
0 commit comments