@@ -63,8 +63,8 @@ public function installNodeModules(string $path, SymfonyStyle $io, bool $isVerbo
6363 chdir ($ currentDir );
6464 return true ;
6565 } catch (\Exception $ e ) {
66- $ io ->error ('Failed to install node modules: ' . $ e ->getMessage ());
6766 chdir ($ currentDir );
67+ $ this ->diagnoseAndReportNpmFailure ($ path , $ io , $ e );
6868 return false ;
6969 }
7070 }
@@ -135,4 +135,201 @@ public function checkOutdatedPackages(string $path, SymfonyStyle $io): void
135135
136136 chdir ($ currentDir );
137137 }
138+
139+ /**
140+ * Diagnose npm installation failure and provide specific error messages
141+ *
142+ * @param string $path
143+ * @param SymfonyStyle $io
144+ * @param \Exception $exception
145+ * @return void
146+ */
147+ private function diagnoseAndReportNpmFailure (string $ path , SymfonyStyle $ io , \Exception $ exception ): void
148+ {
149+ $ io ->error ('Failed to install node modules. ' );
150+ $ io ->newLine ();
151+
152+ // Check 1: npm availability
153+ if (!$ this ->isCommandAvailable ('npm ' )) {
154+ $ io ->error ('npm is not installed or not available in PATH. ' );
155+ $ io ->writeln ('<fg=yellow>Fix:</> ' );
156+ $ io ->writeln (' Install Node.js and npm: https://nodejs.org/ ' );
157+ return ;
158+ }
159+
160+ // Check 2: node availability
161+ if (!$ this ->isCommandAvailable ('node ' )) {
162+ $ io ->error ('Node.js is not installed or not available in PATH. ' );
163+ $ io ->writeln ('<fg=yellow>Fix:</> ' );
164+ $ io ->writeln (' Install Node.js: https://nodejs.org/ ' );
165+ return ;
166+ }
167+
168+ // Check 3: package.json validity
169+ if (!$ this ->isPackageJsonValid ($ path )) {
170+ $ io ->error ('package.json is missing or contains invalid JSON. ' );
171+ $ io ->writeln ('<fg=yellow>Fix:</> ' );
172+ $ io ->writeln (' Verify package.json exists at: ' . $ path . '/package.json ' );
173+ return ;
174+ }
175+
176+ // Check 4: package-lock.json corruption
177+ if ($ this ->fileDriver ->isExists ($ path . '/package-lock.json ' )) {
178+ if (!$ this ->isPackageLockValid ($ path )) {
179+ $ io ->error ('package-lock.json is corrupted or invalid. ' );
180+ $ io ->writeln ('<fg=yellow>Fix:</> ' );
181+ $ io ->writeln (' rm -rf node_modules package-lock.json ' );
182+ $ io ->writeln (' npm install ' );
183+ return ;
184+ }
185+ }
186+
187+ // Check 5: Permission issues
188+ if ($ this ->hasPermissionIssues ($ path )) {
189+ $ io ->error ('Permission denied when accessing node_modules directory. ' );
190+ $ io ->writeln ('<fg=yellow>Fix:</> ' );
191+ $ io ->writeln (' sudo chown -R $(whoami) ' . $ path . '/node_modules ' );
192+ $ io ->writeln (' Or delete and reinstall: rm -rf node_modules && npm install ' );
193+ return ;
194+ }
195+
196+ // Check 6: Disk space
197+ if (!$ this ->hasSufficientDiskSpace ($ path )) {
198+ $ io ->error ('Insufficient disk space to install node modules. ' );
199+ $ io ->writeln ('<fg=yellow>Fix:</> ' );
200+ $ io ->writeln (' Free up disk space and try again. ' );
201+ return ;
202+ }
203+
204+ // Check 7: npm cache corruption
205+ $ errorMessage = $ exception ->getMessage ();
206+ if (str_contains ($ errorMessage , 'EINTEGRITY ' ) || str_contains ($ errorMessage , 'sha ' )) {
207+ $ io ->error ('npm cache is corrupted (integrity checksum mismatch). ' );
208+ $ io ->writeln ('<fg=yellow>Fix:</> ' );
209+ $ io ->writeln (' npm cache clean --force ' );
210+ $ io ->writeln (' rm -rf node_modules package-lock.json ' );
211+ $ io ->writeln (' npm install ' );
212+ return ;
213+ }
214+
215+ // Check 8: Network issues
216+ if (str_contains ($ errorMessage , 'ENOTFOUND ' ) || str_contains ($ errorMessage , 'ETIMEDOUT ' )) {
217+ $ io ->error ('Network error: Cannot reach npm registry. ' );
218+ $ io ->writeln ('<fg=yellow>Fix:</> ' );
219+ $ io ->writeln (' Check your internet connection and try again. ' );
220+ $ io ->writeln (' Or configure a different registry: npm config set registry https://registry.npmjs.org/ ' );
221+ return ;
222+ }
223+
224+ // Fallback: Show actual npm error
225+ $ io ->error ('npm install failed with an unknown error. ' );
226+ $ io ->writeln ('<fg=yellow>Error details:</> ' );
227+ $ io ->writeln (' ' . $ exception ->getMessage ());
228+ $ io ->newLine ();
229+ $ io ->writeln ('<fg=yellow>Suggested fix:</> ' );
230+ $ io ->writeln (' 1. npm cache clean --force ' );
231+ $ io ->writeln (' 2. rm -rf node_modules package-lock.json ' );
232+ $ io ->writeln (' 3. npm install ' );
233+ }
234+
235+ /**
236+ * Check if a command is available
237+ *
238+ * @param string $command
239+ * @return bool
240+ */
241+ private function isCommandAvailable (string $ command ): bool
242+ {
243+ try {
244+ $ this ->shell ->execute ('which ' . $ command . ' > /dev/null 2>&1 ' );
245+ return true ;
246+ } catch (\Exception $ e ) {
247+ return false ;
248+ }
249+ }
250+
251+ /**
252+ * Check if package.json is valid
253+ *
254+ * @param string $path
255+ * @return bool
256+ */
257+ private function isPackageJsonValid (string $ path ): bool
258+ {
259+ $ packageJsonPath = $ path . '/package.json ' ;
260+ if (!$ this ->fileDriver ->isExists ($ packageJsonPath )) {
261+ return false ;
262+ }
263+
264+ try {
265+ $ content = $ this ->fileDriver ->fileGetContents ($ packageJsonPath );
266+ json_decode ($ content , true );
267+ return json_last_error () === JSON_ERROR_NONE ;
268+ } catch (\Exception $ e ) {
269+ return false ;
270+ }
271+ }
272+
273+ /**
274+ * Check if package-lock.json is valid
275+ *
276+ * @param string $path
277+ * @return bool
278+ */
279+ private function isPackageLockValid (string $ path ): bool
280+ {
281+ $ lockPath = $ path . '/package-lock.json ' ;
282+ if (!$ this ->fileDriver ->isExists ($ lockPath )) {
283+ return true ; // If it doesn't exist, it's not invalid
284+ }
285+
286+ try {
287+ $ content = $ this ->fileDriver ->fileGetContents ($ lockPath );
288+ json_decode ($ content , true );
289+ return json_last_error () === JSON_ERROR_NONE ;
290+ } catch (\Exception $ e ) {
291+ return false ;
292+ }
293+ }
294+
295+ /**
296+ * Check for permission issues in node_modules
297+ *
298+ * @param string $path
299+ * @return bool
300+ */
301+ private function hasPermissionIssues (string $ path ): bool
302+ {
303+ $ nodeModulesPath = $ path . '/node_modules ' ;
304+ if (!$ this ->fileDriver ->isDirectory ($ nodeModulesPath )) {
305+ return false ; // Directory doesn't exist yet, so no permission issues
306+ }
307+
308+ try {
309+ // Try to write a test file
310+ $ testFile = $ nodeModulesPath . '/.write-test- ' . time ();
311+ $ this ->fileDriver ->filePutContents ($ testFile , 'test ' );
312+ $ this ->fileDriver ->deleteFile ($ testFile );
313+ return false ;
314+ } catch (\Exception $ e ) {
315+ return true ;
316+ }
317+ }
318+
319+ /**
320+ * Check if there is sufficient disk space
321+ *
322+ * @param string $path
323+ * @return bool
324+ */
325+ private function hasSufficientDiskSpace (string $ path ): bool
326+ {
327+ try {
328+ $ freeSpace = disk_free_space ($ path );
329+ // Require at least 100MB free space
330+ return $ freeSpace !== false && $ freeSpace > 100 * 1024 * 1024 ;
331+ } catch (\Exception $ e ) {
332+ return true ; // If we can't check, assume it's fine
333+ }
334+ }
138335}
0 commit comments