@@ -3,8 +3,13 @@ import { PluginLoadError, PluginNotFoundError } from './errors';
33import { GeneratorRegistry } from './registry' ;
44import { isGeneratorPlugin } from './type-guards' ;
55import { logger } from '@nx/devkit' ;
6- import { detectCi , installPackages } from './auto-installer' ;
6+ import {
7+ detectCi ,
8+ detectPackageManager ,
9+ installPackages ,
10+ } from './auto-installer' ;
711import { isLocalDev } from './utils/is-local-dev' ;
12+ import * as readline from 'node:readline' ;
813
914const BUILTIN_PLUGIN_MAP : Record < string , string > = {
1015 'openapi-tools' : '@nx-plugin-openapi/plugin-openapi' ,
@@ -30,9 +35,51 @@ function shouldTryAutoInstall(error: unknown, packageName: string): boolean {
3035 ) ;
3136}
3237
38+ /**
39+ * Extracts a clean error message without stack trace for user-facing logs.
40+ * Full error details are logged via logger.debug for verbose mode.
41+ */
42+ function getCleanErrorMessage ( error : unknown ) : string {
43+ if ( error instanceof Error ) {
44+ return error . message ;
45+ }
46+ return String ( error ) ;
47+ }
48+
49+ /**
50+ * Prompts the user for confirmation before auto-installing a package.
51+ * Returns true if user confirms (y/Y/yes), false otherwise.
52+ * In non-interactive environments, returns false.
53+ */
54+ async function promptForInstall ( packageName : string ) : Promise < boolean > {
55+ // Check if stdin is a TTY (interactive terminal)
56+ if ( ! process . stdin . isTTY ) {
57+ logger . debug ( 'Non-interactive environment detected, skipping prompt' ) ;
58+ return false ;
59+ }
60+
61+ const pm = detectPackageManager ( ) ;
62+
63+ return new Promise ( ( resolve ) => {
64+ const rl = readline . createInterface ( {
65+ input : process . stdin ,
66+ output : process . stdout ,
67+ } ) ;
68+
69+ rl . question (
70+ `\nPlugin '${ packageName } ' is not installed.\nWould you like to install it using ${ pm } ? (y/n) ` ,
71+ ( answer ) => {
72+ rl . close ( ) ;
73+ const normalizedAnswer = answer . trim ( ) . toLowerCase ( ) ;
74+ resolve ( normalizedAnswer === 'y' || normalizedAnswer === 'yes' ) ;
75+ }
76+ ) ;
77+ } ) ;
78+ }
79+
3380export async function loadPlugin (
3481 name : string ,
35- opts ?: { root ?: string }
82+ opts ?: { root ?: string ; skipPrompt ?: boolean }
3683) : Promise < GeneratorPlugin > {
3784 logger . debug ( `Loading plugin: ${ name } ` ) ;
3885
@@ -110,29 +157,40 @@ export async function loadPlugin(
110157 const isModuleNotFound =
111158 code === 'ERR_MODULE_NOT_FOUND' || / C a n n o t f i n d m o d u l e / . test ( msg ) ;
112159
160+ // Log clean message for users, full details in debug mode
113161 logger . debug ( `Failed to load plugin from node_modules: ${ e } ` ) ;
114162
115163 // 2. If module not found and auto-install conditions are met, try auto-installation
116164 if ( isModuleNotFound && shouldTryAutoInstall ( e , pkg ) ) {
117- logger . info (
118- `Plugin ${ pkg } not found in node_modules. Attempting to auto-install...`
119- ) ;
120- try {
121- installPackages ( [ pkg ] , { dev : true } ) ;
122- logger . info ( `Successfully installed ${ pkg } , loading plugin...` ) ;
123-
124- // Retry the import after installation
125- const retryMod = await import ( pkg ) ;
126- const plugin = await loadFromModule ( retryMod ) ;
127-
128- logger . info (
129- `Successfully loaded plugin after auto-installation: ${ name } `
130- ) ;
131- cache . set ( name , plugin ) ;
132- return plugin ;
133- } catch ( installError ) {
134- logger . warn ( `Auto-installation failed for ${ pkg } : ${ installError } ` ) ;
135- // Continue to fallback paths for local development
165+ // Prompt user for confirmation (unless skipped via option)
166+ const shouldInstall = opts ?. skipPrompt
167+ ? true
168+ : await promptForInstall ( pkg ) ;
169+
170+ if ( shouldInstall ) {
171+ const pm = detectPackageManager ( ) ;
172+ logger . info ( `Installing ${ pkg } using ${ pm } ...` ) ;
173+ try {
174+ installPackages ( [ pkg ] , { dev : true } ) ;
175+ logger . info ( `Successfully installed ${ pkg } ` ) ;
176+
177+ // Retry the import after installation
178+ const retryMod = await import ( pkg ) ;
179+ const plugin = await loadFromModule ( retryMod ) ;
180+
181+ logger . info ( `Successfully loaded plugin: ${ name } ` ) ;
182+ cache . set ( name , plugin ) ;
183+ return plugin ;
184+ } catch ( installError ) {
185+ // Show clean message to user, full details in debug
186+ logger . debug ( `Full installation error: ${ installError } ` ) ;
187+ logger . warn (
188+ `Auto-installation failed for ${ pkg } : ${ getCleanErrorMessage ( installError ) } `
189+ ) ;
190+ // Continue to fallback paths for local development
191+ }
192+ } else {
193+ logger . info ( `Skipping installation of ${ pkg } ` ) ;
136194 }
137195 }
138196
@@ -179,15 +237,15 @@ export async function loadPlugin(
179237
180238 // 4. If all attempts failed, throw appropriate error
181239 if ( isModuleNotFound ) {
182- logger . error (
183- `Plugin not found: ${ name } . Searched paths: ${ JSON . stringify (
184- searchPaths
185- ) } `
186- ) ;
240+ // User-friendly message without technical details
241+ logger . error ( `Plugin not found: ${ name } ` ) ;
242+ logger . debug ( `Searched paths: ${ JSON . stringify ( searchPaths ) } ` ) ;
187243 throw new PluginNotFoundError ( name , searchPaths ) ;
188244 }
189245
190- logger . error ( `Failed to load plugin: ${ name } . Error: ${ e } ` ) ;
246+ // User-friendly error, full details in debug
247+ logger . error ( `Failed to load plugin: ${ name } . ${ getCleanErrorMessage ( e ) } ` ) ;
248+ logger . debug ( `Full error details: ${ e } ` ) ;
191249 throw new PluginLoadError ( name , e ) ;
192250 }
193251}
0 commit comments