1+ import { readdirSync , readFileSync , existsSync } from 'fs' ;
2+ import { join } from 'path' ;
3+ import path from 'path' ;
4+
5+ export function createPluginDiscoveryPlugin ( ) {
6+ return {
7+ name : 'plugin-discovery' ,
8+ setup ( build ) {
9+ // Generate the workflow loaders file before build starts
10+ build . onStart ( async ( ) => {
11+ try {
12+ await generateWorkflowLoaders ( ) ;
13+ } catch ( error ) {
14+ console . error ( 'Failed to generate workflow loaders:' , error ) ;
15+ throw error ;
16+ }
17+ } ) ;
18+ }
19+ } ;
20+ }
21+
22+ async function generateWorkflowLoaders ( ) {
23+ const pluginsDir = path . resolve ( process . cwd ( ) , 'src/plugins' ) ;
24+
25+ if ( ! existsSync ( pluginsDir ) ) {
26+ throw new Error ( `Plugins directory not found: ${ pluginsDir } ` ) ;
27+ }
28+
29+ // Scan for workflow directories
30+ const workflowDirs = readdirSync ( pluginsDir , { withFileTypes : true } )
31+ . filter ( dirent => dirent . isDirectory ( ) )
32+ . map ( dirent => dirent . name ) ;
33+
34+ const workflowLoaders = { } ;
35+ const workflowMetadata = { } ;
36+
37+ for ( const dirName of workflowDirs ) {
38+ const dirPath = join ( pluginsDir , dirName ) ;
39+ const indexPath = join ( dirPath , 'index.ts' ) ;
40+
41+ // Check if workflow has index.ts file
42+ if ( ! existsSync ( indexPath ) ) {
43+ console . warn ( `Skipping ${ dirName } : no index.ts file found` ) ;
44+ continue ;
45+ }
46+
47+ // Try to extract workflow metadata from index.ts
48+ try {
49+ const indexContent = readFileSync ( indexPath , 'utf8' ) ;
50+ const metadata = extractWorkflowMetadata ( indexContent ) ;
51+
52+ if ( metadata ) {
53+ // Find all tool files in this workflow directory
54+ const toolFiles = readdirSync ( dirPath , { withFileTypes : true } )
55+ . filter ( dirent => dirent . isFile ( ) )
56+ . map ( dirent => dirent . name )
57+ . filter ( name =>
58+ ( name . endsWith ( '.ts' ) || name . endsWith ( '.js' ) ) &&
59+ name !== 'index.ts' &&
60+ name !== 'index.js' &&
61+ ! name . endsWith ( '.test.ts' ) &&
62+ ! name . endsWith ( '.test.js' ) &&
63+ name !== 'active-processes.ts' // Special exclusion for swift-package
64+ ) ;
65+
66+ // Generate dynamic loader function that loads workflow and all its tools
67+ workflowLoaders [ dirName ] = generateWorkflowLoader ( dirName , toolFiles ) ;
68+ workflowMetadata [ dirName ] = metadata ;
69+
70+ console . log ( `✅ Discovered workflow: ${ dirName } - ${ metadata . name } (${ toolFiles . length } tools)` ) ;
71+ } else {
72+ console . warn ( `⚠️ Skipping ${ dirName } : invalid workflow metadata` ) ;
73+ }
74+ } catch ( error ) {
75+ console . warn ( `⚠️ Error processing ${ dirName } :` , error ) ;
76+ }
77+ }
78+
79+ // Generate the content for generated-plugins.ts
80+ const generatedContent = generatePluginsFileContent ( workflowLoaders , workflowMetadata ) ;
81+
82+ // Write to the generated file
83+ const outputPath = path . resolve ( process . cwd ( ) , 'src/core/generated-plugins.ts' ) ;
84+
85+ const fs = await import ( 'fs' ) ;
86+ await fs . promises . writeFile ( outputPath , generatedContent , 'utf8' ) ;
87+
88+ console . log ( `🔧 Generated workflow loaders for ${ Object . keys ( workflowLoaders ) . length } workflows` ) ;
89+ }
90+
91+ function generateWorkflowLoader ( workflowName , toolFiles ) {
92+ const toolImports = toolFiles . map ( ( file , index ) => {
93+ const toolName = file . replace ( / \. ( t s | j s ) $ / , '' ) ;
94+ return `const tool_${ index } = await import('../plugins/${ workflowName } /${ toolName } .js').then(m => m.default)` ;
95+ } ) . join ( ';\n ' ) ;
96+
97+ const toolExports = toolFiles . map ( ( file , index ) => {
98+ const toolName = file . replace ( / \. ( t s | j s ) $ / , '' ) ;
99+ return `'${ toolName } ': tool_${ index } ` ;
100+ } ) . join ( ',\n ' ) ;
101+
102+ return `async () => {
103+ const { workflow } = await import('../plugins/${ workflowName } /index.js');
104+ ${ toolImports ? toolImports + ';\n ' : '' }
105+ return {
106+ workflow,
107+ ${ toolExports ? toolExports : '' }
108+ };
109+ }` ;
110+ }
111+
112+ function extractWorkflowMetadata ( content ) {
113+ try {
114+ // Simple regex to extract workflow export object
115+ const workflowMatch = content . match ( / e x p o r t \s + c o n s t \s + w o r k f l o w \s * = \s * ( { [ \s \S ] * ?} ) ; / ) ;
116+
117+ if ( ! workflowMatch ) {
118+ return null ;
119+ }
120+
121+ const workflowObj = workflowMatch [ 1 ] ;
122+
123+ // Extract name
124+ const nameMatch = workflowObj . match ( / n a m e \s * : \s * [ ' " ` ] ( [ ^ ' " ` ] + ) [ ' " ` ] / ) ;
125+ if ( ! nameMatch ) return null ;
126+
127+ // Extract description
128+ const descMatch = workflowObj . match ( / d e s c r i p t i o n \s * : \s * [ ' " ` ] ( [ \s \S ] * ?) [ ' " ` ] / ) ;
129+ if ( ! descMatch ) return null ;
130+
131+ // Extract platforms (optional)
132+ const platformsMatch = workflowObj . match ( / p l a t f o r m s \s * : \s * \[ ( [ ^ \] ] * ) \] / ) ;
133+ let platforms ;
134+ if ( platformsMatch ) {
135+ platforms = platformsMatch [ 1 ]
136+ . split ( ',' )
137+ . map ( p => p . trim ( ) . replace ( / [ ' " ] / g, '' ) )
138+ . filter ( p => p . length > 0 ) ;
139+ }
140+
141+ // Extract targets (optional)
142+ const targetsMatch = workflowObj . match ( / t a r g e t s \s * : \s * \[ ( [ ^ \] ] * ) \] / ) ;
143+ let targets ;
144+ if ( targetsMatch ) {
145+ targets = targetsMatch [ 1 ]
146+ . split ( ',' )
147+ . map ( t => t . trim ( ) . replace ( / [ ' " ] / g, '' ) )
148+ . filter ( t => t . length > 0 ) ;
149+ }
150+
151+ // Extract projectTypes (optional)
152+ const projectTypesMatch = workflowObj . match ( / p r o j e c t T y p e s \s * : \s * \[ ( [ ^ \] ] * ) \] / ) ;
153+ let projectTypes ;
154+ if ( projectTypesMatch ) {
155+ projectTypes = projectTypesMatch [ 1 ]
156+ . split ( ',' )
157+ . map ( pt => pt . trim ( ) . replace ( / [ ' " ] / g, '' ) )
158+ . filter ( pt => pt . length > 0 ) ;
159+ }
160+
161+ // Extract capabilities (optional)
162+ const capabilitiesMatch = workflowObj . match ( / c a p a b i l i t i e s \s * : \s * \[ ( [ ^ \] ] * ) \] / ) ;
163+ let capabilities ;
164+ if ( capabilitiesMatch ) {
165+ capabilities = capabilitiesMatch [ 1 ]
166+ . split ( ',' )
167+ . map ( c => c . trim ( ) . replace ( / [ ' " ] / g, '' ) )
168+ . filter ( c => c . length > 0 ) ;
169+ }
170+
171+ const result = {
172+ name : nameMatch [ 1 ] ,
173+ description : descMatch [ 1 ]
174+ } ;
175+
176+ if ( platforms ) result . platforms = platforms ;
177+ if ( targets ) result . targets = targets ;
178+ if ( projectTypes ) result . projectTypes = projectTypes ;
179+ if ( capabilities ) result . capabilities = capabilities ;
180+
181+ return result ;
182+ } catch ( error ) {
183+ console . warn ( 'Failed to extract workflow metadata:' , error ) ;
184+ return null ;
185+ }
186+ }
187+
188+ function generatePluginsFileContent ( workflowLoaders , workflowMetadata ) {
189+ const loaderEntries = Object . entries ( workflowLoaders )
190+ . map ( ( [ key , loader ] ) => {
191+ // Indent the loader function properly
192+ const indentedLoader = loader
193+ . split ( '\n' )
194+ . map ( ( line , index ) => index === 0 ? ` '${ key } ': ${ line } ` : ` ${ line } ` )
195+ . join ( '\n' ) ;
196+ return indentedLoader ;
197+ } )
198+ . join ( ',\n' ) ;
199+
200+ const metadataEntries = Object . entries ( workflowMetadata )
201+ . map ( ( [ key , metadata ] ) => {
202+ const metadataJson = JSON . stringify ( metadata , null , 4 )
203+ . split ( '\n' )
204+ . map ( line => ` ${ line } ` )
205+ . join ( '\n' ) ;
206+ return ` '${ key } ': ${ metadataJson . trim ( ) } ` ;
207+ } )
208+ . join ( ',\n' ) ;
209+
210+ return `// AUTO-GENERATED - DO NOT EDIT
211+ // This file is generated by the plugin discovery esbuild plugin
212+
213+ /* eslint-disable @typescript-eslint/explicit-function-return-type */
214+
215+ // Generated based on filesystem scan
216+ export const WORKFLOW_LOADERS = {
217+ ${ loaderEntries }
218+ };
219+
220+ export type WorkflowName = keyof typeof WORKFLOW_LOADERS;
221+
222+ // Optional: Export workflow metadata for quick access
223+ export const WORKFLOW_METADATA = {
224+ ${ metadataEntries }
225+ };
226+ ` ;
227+ }
0 commit comments