@@ -57,12 +57,18 @@ component singleton {
5757 value : " opencode"
5858 }
5959 ]
60+
61+ // Compiled once (singleton) — matches any public function declaration
62+ FUNCTION_PATTERN = createObject ( " java" , " java.util.regex.Pattern" ).compile (
63+ " (?i)(?:^|\s)(?:public\s+)?(?:\w+\s+)?function\s+(\w+)\s*\("
64+ )
6065 }
6166
6267 // Expose them as instance properties for easier access in commands
6368 this .SUPPORTED_AGENTS = static .SUPPORTED_AGENTS
6469 this .AGENT_OPTIONS = static .AGENT_OPTIONS
6570 this .AGENT_FILES = static .AGENT_FILES
71+ this .FUNCTION_PATTERN = static .FUNCTION_PATTERN
6672
6773 /**
6874 * Configure agents for a project
@@ -418,16 +424,20 @@ component singleton {
418424 )
419425
420426 // Generate handlers snapshot
421- var handlersSnapshotContent = generateHandlersSnapshot (
422- arguments .directory ,
423- arguments .templateType
424- )
425- content = replaceNoCase (
426- content ,
427- " |HANDLERS_SNAPSHOT|" ,
428- handlersSnapshotContent ,
429- " all"
430- )
427+ var handlersSnapshotContent = generateHandlersSnapshot ( arguments .directory , arguments .templateType )
428+ content = replaceNoCase ( content , " |HANDLERS_SNAPSHOT|" , handlersSnapshotContent , " all" )
429+
430+ // Generate interceptors snapshot
431+ var interceptorsSnapshotContent = generateInterceptorsSnapshot ( arguments .directory , arguments .templateType )
432+ content = replaceNoCase ( content , " |INTERCEPTORS_SNAPSHOT|" , interceptorsSnapshotContent , " all" )
433+
434+ // Generate layouts snapshot
435+ var layoutsSnapshotContent = generateLayoutsSnapshot ( arguments .directory , arguments .templateType )
436+ content = replaceNoCase ( content , " |LAYOUTS_SNAPSHOT|" , layoutsSnapshotContent , " all" )
437+
438+ // Generate custom modules snapshot
439+ var customModulesContent = generateCustomModulesSnapshot ( arguments .directory , arguments .templateType )
440+ content = replaceNoCase ( content , " |CUSTOM_MODULES_SNAPSHOT|" , customModulesContent , " all" )
431441
432442 // Add guidelines inventory (module and additional guidelines only)
433443 var guidelinesContent = generateGuidelinesContent (
@@ -876,43 +886,187 @@ component singleton {
876886 " onapplicationend"
877887 ]
878888
879- // Use a Java regex to extract public function names
880- var pattern = createObject ( " java" , " java.util.regex.Pattern" ).compile (
881- " (?i)(?:^|\s)(?:public\s+)?(?:\w+\s+)?function\s+(\w+)\s*\("
882- )
883-
884- var handlerFiles = directoryList (
885- handlersRoot ,
886- false ,
887- " path" ,
888- " *.cfc|*.bx"
889- )
890- var lines = []
889+ var handlerFiles = directoryList ( handlersRoot , false , " path" , " *.cfc|*.bx" )
890+ var lines = []
891891
892892 for ( var handlerFile in handlerFiles ) {
893893 var handlerName = listFirst ( getFileFromPath ( handlerFile ), " ." )
894- var source = fileRead ( handlerFile )
895- var matcher = pattern .matcher ( source )
896- var actions = []
897-
898- while ( matcher .find () ) {
899- var fnName = matcher .group ( 1 )
900- if ( ! lifecycleMethods .findNoCase ( fnName ) ) {
901- if ( ! actions .findNoCase ( fnName ) ) {
902- actions .append ( fnName )
903- }
904- }
894+ var actions = extractFunctionNames ( fileRead ( handlerFile ), lifecycleMethods )
895+
896+ lines .append (
897+ actions .len ()
898+ ? " - **#handlerName #**: #actions .toList ( ' , ' ) #"
899+ : " - **#handlerName #**: _(no public actions)_"
900+ )
901+ }
902+
903+ if ( ! lines .len () ) {
904+ return " No handlers found."
905+ }
906+
907+ lines .sort ( " textnocase" )
908+ return lines .toList ( chr ( 10 ) )
909+ }
910+
911+ /**
912+ * Generate a snapshot of existing interceptors and their interception point methods
913+ *
914+ * @directory The project directory
915+ * @templateType "modern" or "flat"
916+ *
917+ * @return Formatted markdown bullet list of interceptors and their announced points
918+ */
919+ private function generateInterceptorsSnapshot(
920+ required string directory ,
921+ required string templateType
922+ ){
923+ var interceptorsRoot = arguments .templateType == " modern"
924+ ? " #arguments .directory #/app/interceptors"
925+ : " #arguments .directory #/interceptors"
926+
927+ if ( ! directoryExists ( interceptorsRoot ) ) {
928+ return " No interceptors found."
929+ }
930+
931+ // Methods to exclude — framework inherited methods, not interception points
932+ var excludedMethods = [
933+ " init" ,
934+ " configure" ,
935+ " getproperty" ,
936+ " setproperty" ,
937+ " getproperties"
938+ ]
939+
940+ var interceptorFiles = directoryList ( interceptorsRoot , false , " path" , " *.cfc|*.bx" )
941+ var lines = []
942+
943+ for ( var interceptorFile in interceptorFiles ) {
944+ var interceptorName = listFirst ( getFileFromPath ( interceptorFile ), " ." )
945+ var points = extractFunctionNames ( fileRead ( interceptorFile ), excludedMethods )
946+
947+ lines .append (
948+ points .len ()
949+ ? " - **#interceptorName #**: #points .toList ( ' , ' ) #"
950+ : " - **#interceptorName #**: _(no interception points declared)_"
951+ )
952+ }
953+
954+ if ( ! lines .len () ) {
955+ return " No interceptors found."
956+ }
957+
958+ lines .sort ( " textnocase" )
959+ return lines .toList ( chr ( 10 ) )
960+ }
961+
962+ /**
963+ * Extract unique public function names from CFML/BoxLang source using the shared compiled pattern.
964+ * The matched names are filtered against the provided exclusion list.
965+ *
966+ * @source Raw source code string to scan
967+ * @excludedMethods Array of method names (case-insensitive) to omit from results
968+ *
969+ * @return Ordered array of unique, non-excluded function names found in the source
970+ */
971+ private array function extractFunctionNames(
972+ required string source ,
973+ required array excludedMethods
974+ ){
975+ var matcher = variables .FUNCTION_PATTERN .matcher ( arguments .source )
976+ var names = []
977+
978+ while ( matcher .find () ) {
979+ var fnName = matcher .group ( 1 )
980+ if ( ! arguments .excludedMethods .findNoCase ( fnName ) && ! names .findNoCase ( fnName ) ) {
981+ names .append ( fnName )
982+ }
983+ }
984+
985+ return names
986+ }
987+
988+ /**
989+ * Generate a snapshot of available layouts
990+ *
991+ * @directory The project directory
992+ * @templateType "modern" or "flat"
993+ *
994+ * @return Formatted markdown bullet list of layout files
995+ */
996+ private function generateLayoutsSnapshot(
997+ required string directory ,
998+ required string templateType
999+ ){
1000+ var layoutsRoot = arguments .templateType == " modern"
1001+ ? " #arguments .directory #/app/layouts"
1002+ : " #arguments .directory #/layouts"
1003+
1004+ if ( ! directoryExists ( layoutsRoot ) ) {
1005+ return " No layouts found."
1006+ }
1007+
1008+ var layoutFiles = directoryList ( layoutsRoot , false , " path" , " *.cfm|*.bxm" )
1009+ var lines = []
1010+
1011+ for ( var layoutFile in layoutFiles ) {
1012+ var layoutName = getFileFromPath ( layoutFile )
1013+ lines .append ( " - **#layoutName #**" )
1014+ }
1015+
1016+ if ( ! lines .len () ) {
1017+ return " No layouts found."
1018+ }
1019+
1020+ lines .sort ( " textnocase" )
1021+ return lines .toList ( chr ( 10 ) )
1022+ }
1023+
1024+ /**
1025+ * Generate a snapshot of application-level custom modules
1026+ *
1027+ * Modern template checks: /app/modules
1028+ * Flat template checks: /modules_app
1029+ * Both check the alternate as a fallback if the primary is missing or empty.
1030+ *
1031+ * @directory The project directory
1032+ * @templateType "modern" or "flat"
1033+ *
1034+ * @return Formatted markdown bullet list of custom module names
1035+ */
1036+ private function generateCustomModulesSnapshot(
1037+ required string directory ,
1038+ required string templateType
1039+ ){
1040+ // Build candidate paths (primary first, then fallback)
1041+ var candidates = arguments .templateType == " modern"
1042+ ? [ " #arguments .directory #/app/modules" , " #arguments .directory #/modules_app" ]
1043+ : [ " #arguments .directory #/modules_app" , " #arguments .directory #/app/modules" ]
1044+
1045+ var lines = []
1046+
1047+ for ( var modulesRoot in candidates ) {
1048+ if ( ! directoryExists ( modulesRoot ) ) {
1049+ continue ;
9051050 }
9061051
907- if ( actions .len () ) {
908- lines .append ( " - **#handlerName #**: #actions .toList ( " , " ) #" )
909- } else {
910- lines .append ( " - **#handlerName #**: _(no public actions)_" )
1052+ // Each subdirectory is a module
1053+ var moduleDirs = directoryList ( modulesRoot , false , " path" )
1054+ if ( ! moduleDirs .len () ) {
1055+ continue ;
1056+ }
1057+
1058+ var label = modulesRoot .replace ( arguments .directory , " " ).replaceAll ( " ^[/\\]" , " " )
1059+
1060+ for ( var moduleDir in moduleDirs ) {
1061+ if ( directoryExists ( moduleDir ) ) {
1062+ var moduleName = getFileFromPath ( moduleDir )
1063+ lines .append ( " - **#moduleName #** (`#label #`)" )
1064+ }
9111065 }
9121066 }
9131067
9141068 if ( ! lines .len () ) {
915- return " No handlers found."
1069+ return " No custom modules found."
9161070 }
9171071
9181072 lines .sort ( " textnocase" )
0 commit comments