@@ -39,6 +39,7 @@ const { CLIUtils } = require('../../../lib/cli-utils');
3939const { ManifestGenerator } = require ( './manifest-generator' ) ;
4040const { IdeConfigManager } = require ( './ide-config-manager' ) ;
4141const { replaceAgentSidecarFolders } = require ( './post-install-sidecar-replacement' ) ;
42+ const { CustomHandler } = require ( '../custom/handler' ) ;
4243
4344class Installer {
4445 constructor ( ) {
@@ -407,7 +408,10 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
407408 * @param {string[] } config.ides - IDEs to configure
408409 * @param {boolean } config.skipIde - Skip IDE configuration
409410 */
410- async install ( config ) {
411+ async install ( originalConfig ) {
412+ // Clone config to avoid mutating the caller's object
413+ const config = { ...originalConfig } ;
414+
411415 // Display BMAD logo
412416 CLIUtils . displayLogo ( ) ;
413417
@@ -440,7 +444,6 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
440444
441445 // Handle selectedFiles (from existing install path or manual directory input)
442446 if ( config . customContent && config . customContent . selected && config . customContent . selectedFiles ) {
443- const { CustomHandler } = require ( '../custom/handler' ) ;
444447 const customHandler = new CustomHandler ( ) ;
445448 for ( const customFile of config . customContent . selectedFiles ) {
446449 const customInfo = await customHandler . getCustomInfo ( customFile , path . resolve ( config . directory ) ) ;
@@ -837,9 +840,8 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
837840 // Regular custom content from user input (non-cached)
838841 if ( finalCustomContent && finalCustomContent . selected && finalCustomContent . selectedFiles ) {
839842 // Add custom modules to the installation list
843+ const customHandler = new CustomHandler ( ) ;
840844 for ( const customFile of finalCustomContent . selectedFiles ) {
841- const { CustomHandler } = require ( '../custom/handler' ) ;
842- const customHandler = new CustomHandler ( ) ;
843845 const customInfo = await customHandler . getCustomInfo ( customFile , projectDir ) ;
844846 if ( customInfo && customInfo . id ) {
845847 allModules . push ( customInfo . id ) ;
@@ -929,7 +931,6 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
929931
930932 // Finally check regular custom content
931933 if ( ! isCustomModule && finalCustomContent && finalCustomContent . selected && finalCustomContent . selectedFiles ) {
932- const { CustomHandler } = require ( '../custom/handler' ) ;
933934 const customHandler = new CustomHandler ( ) ;
934935 for ( const customFile of finalCustomContent . selectedFiles ) {
935936 const info = await customHandler . getCustomInfo ( customFile , projectDir ) ;
@@ -943,7 +944,6 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
943944
944945 if ( isCustomModule && customInfo ) {
945946 // Install custom module using CustomHandler but as a proper module
946- const { CustomHandler } = require ( '../custom/handler' ) ;
947947 const customHandler = new CustomHandler ( ) ;
948948
949949 // Install to module directory instead of custom directory
@@ -972,19 +972,39 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
972972 if ( await fs . pathExists ( customDir ) ) {
973973 // Move contents to module directory
974974 const items = await fs . readdir ( customDir ) ;
975- for ( const item of items ) {
976- const srcPath = path . join ( customDir , item ) ;
977- const destPath = path . join ( moduleTargetPath , item ) ;
975+ const movedItems = [ ] ;
976+ try {
977+ for ( const item of items ) {
978+ const srcPath = path . join ( customDir , item ) ;
979+ const destPath = path . join ( moduleTargetPath , item ) ;
978980
979- // If destination exists, remove it first (or we could merge)
980- if ( await fs . pathExists ( destPath ) ) {
981- await fs . remove ( destPath ) ;
982- }
981+ // If destination exists, remove it first (or we could merge)
982+ if ( await fs . pathExists ( destPath ) ) {
983+ await fs . remove ( destPath ) ;
984+ }
983985
984- await fs . move ( srcPath , destPath ) ;
986+ await fs . move ( srcPath , destPath ) ;
987+ movedItems . push ( { src : srcPath , dest : destPath } ) ;
988+ }
989+ } catch ( moveError ) {
990+ // Rollback: restore any successfully moved items
991+ for ( const moved of movedItems ) {
992+ try {
993+ await fs . move ( moved . dest , moved . src ) ;
994+ } catch {
995+ // Best-effort rollback - log if it fails
996+ console . error ( `Failed to rollback ${ moved . dest } during cleanup` ) ;
997+ }
998+ }
999+ throw new Error ( `Failed to move custom module files: ${ moveError . message } ` ) ;
9851000 }
9861001 }
987- await fs . remove ( tempCustomPath ) ;
1002+ try {
1003+ await fs . remove ( tempCustomPath ) ;
1004+ } catch ( cleanupError ) {
1005+ // Non-fatal: temp directory cleanup failed but files were moved successfully
1006+ console . warn ( `Warning: Could not clean up temp directory: ${ cleanupError . message } ` ) ;
1007+ }
9881008 }
9891009
9901010 // Create module config (include collected config from module.yaml prompts)
@@ -1066,9 +1086,8 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
10661086 config . customContent . selectedFiles
10671087 ) {
10681088 // Filter out custom modules that were already installed
1089+ const customHandler = new CustomHandler ( ) ;
10691090 for ( const customFile of config . customContent . selectedFiles ) {
1070- const { CustomHandler } = require ( '../custom/handler' ) ;
1071- const customHandler = new CustomHandler ( ) ;
10721091 const customInfo = await customHandler . getCustomInfo ( customFile , projectDir ) ;
10731092
10741093 // Skip if this was installed as a module
@@ -1080,7 +1099,6 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
10801099
10811100 if ( remainingCustomContent . length > 0 ) {
10821101 spinner . start ( 'Installing remaining custom content...' ) ;
1083- const { CustomHandler } = require ( '../custom/handler' ) ;
10841102 const customHandler = new CustomHandler ( ) ;
10851103
10861104 // Use the remaining files
@@ -2581,18 +2599,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
25812599 installedModules ,
25822600 ) ;
25832601
2584- // Handle both old return format (array) and new format (object)
2585- let validCustomModules = [ ] ;
2586- let keptModulesWithoutSources = [ ] ;
2587-
2588- if ( Array . isArray ( customModuleResult ) ) {
2589- // Old format - just an array
2590- validCustomModules = customModuleResult ;
2591- } else if ( customModuleResult && typeof customModuleResult === 'object' ) {
2592- // New format - object with two arrays
2593- validCustomModules = customModuleResult . validCustomModules || [ ] ;
2594- keptModulesWithoutSources = customModuleResult . keptModulesWithoutSources || [ ] ;
2595- }
2602+ const { validCustomModules, keptModulesWithoutSources } = customModuleResult ;
25962603
25972604 const customModulesFromManifest = validCustomModules . map ( ( m ) => ( {
25982605 ...m ,
@@ -3371,7 +3378,10 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
33713378
33723379 // If no missing sources, return immediately
33733380 if ( customModulesWithMissingSources . length === 0 ) {
3374- return validCustomModules ;
3381+ return {
3382+ validCustomModules,
3383+ keptModulesWithoutSources : [ ] ,
3384+ } ;
33753385 }
33763386
33773387 // Stop any spinner for interactive prompts
0 commit comments