create-fluxstack 1.18.1 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +132 -0
- package/LLMD/INDEX.md +1 -1
- package/LLMD/MAINTENANCE.md +197 -197
- package/LLMD/MIGRATION.md +44 -1
- package/LLMD/agent.md +20 -7
- package/LLMD/config/declarative-system.md +268 -268
- package/LLMD/config/environment-vars.md +3 -6
- package/LLMD/config/runtime-reload.md +401 -401
- package/LLMD/core/build-system.md +599 -599
- package/LLMD/core/framework-lifecycle.md +249 -229
- package/LLMD/core/plugin-system.md +154 -100
- package/LLMD/patterns/anti-patterns.md +397 -397
- package/LLMD/patterns/project-structure.md +264 -264
- package/LLMD/patterns/type-safety.md +61 -5
- package/LLMD/reference/cli-commands.md +31 -7
- package/LLMD/reference/plugin-hooks.md +4 -2
- package/LLMD/reference/troubleshooting.md +364 -364
- package/LLMD/resources/controllers.md +465 -465
- package/LLMD/resources/live-auth.md +178 -1
- package/LLMD/resources/live-binary-delta.md +3 -1
- package/LLMD/resources/live-components.md +1192 -1041
- package/LLMD/resources/live-logging.md +3 -1
- package/LLMD/resources/live-rooms.md +1 -1
- package/LLMD/resources/live-upload.md +228 -181
- package/LLMD/resources/plugins-external.md +8 -7
- package/LLMD/resources/rest-auth.md +290 -290
- package/LLMD/resources/routes-eden.md +254 -254
- package/app/client/src/App.tsx +7 -7
- package/app/client/src/components/AppLayout.tsx +60 -23
- package/app/client/src/components/ColorWheel.tsx +195 -0
- package/app/client/src/components/DemoPage.tsx +5 -3
- package/app/client/src/components/LiveUploadWidget.tsx +1 -1
- package/app/client/src/components/ThemePicker.tsx +307 -0
- package/app/client/src/config/theme.config.ts +127 -0
- package/app/client/src/hooks/useThemeClock.ts +66 -0
- package/app/client/src/index.css +193 -0
- package/app/client/src/lib/theme-clock.ts +201 -0
- package/app/client/src/live/AuthDemo.tsx +9 -9
- package/app/client/src/live/CounterDemo.tsx +10 -10
- package/app/client/src/live/FormDemo.tsx +8 -8
- package/app/client/src/live/PingPongDemo.tsx +10 -10
- package/app/client/src/live/RoomChatDemo.tsx +10 -10
- package/app/client/src/live/SharedCounterDemo.tsx +5 -5
- package/app/client/src/pages/ApiTestPage.tsx +5 -5
- package/app/client/src/pages/HomePage.tsx +12 -12
- package/app/server/index.ts +8 -0
- package/app/server/live/auto-generated-components.ts +1 -1
- package/core/build/index.ts +1 -1
- package/core/cli/command-registry.ts +1 -1
- package/core/cli/commands/build.ts +25 -6
- package/core/cli/commands/plugin-deps.ts +1 -2
- package/core/cli/generators/plugin.ts +433 -581
- package/core/framework/server.ts +22 -8
- package/core/index.ts +6 -5
- package/core/plugins/index.ts +71 -199
- package/core/plugins/types.ts +76 -461
- package/core/server/index.ts +1 -1
- package/core/utils/logger/startup-banner.ts +26 -4
- package/create-fluxstack.ts +216 -107
- package/package.json +108 -107
- package/tsconfig.json +2 -1
- package/core/plugins/config.ts +0 -356
- package/core/plugins/dependency-manager.ts +0 -481
- package/core/plugins/discovery.ts +0 -379
- package/core/plugins/executor.ts +0 -353
- package/core/plugins/manager.ts +0 -645
- package/core/plugins/module-resolver.ts +0 -227
- package/core/plugins/registry.ts +0 -913
- package/vitest.config.live.ts +0 -69
package/core/plugins/registry.ts
DELETED
|
@@ -1,913 +0,0 @@
|
|
|
1
|
-
import type { FluxStack, PluginManifest, PluginLoadResult, PluginDiscoveryOptions, PluginPriority } from "./types"
|
|
2
|
-
|
|
3
|
-
type FluxStackPlugin = FluxStack.Plugin
|
|
4
|
-
|
|
5
|
-
const PRIORITY_MAP: Record<string, number> = {
|
|
6
|
-
highest: 1000,
|
|
7
|
-
high: 750,
|
|
8
|
-
normal: 500,
|
|
9
|
-
low: 250,
|
|
10
|
-
lowest: 0
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function normalizePriority(priority?: number | PluginPriority): number {
|
|
14
|
-
if (typeof priority === 'number') return priority
|
|
15
|
-
if (typeof priority === 'string' && priority in PRIORITY_MAP) return PRIORITY_MAP[priority]
|
|
16
|
-
return 500 // default to normal
|
|
17
|
-
}
|
|
18
|
-
import type { FluxStackConfig } from "@config"
|
|
19
|
-
import type { Logger } from "@core/utils/logger"
|
|
20
|
-
import { FluxStackError } from "@core/utils/errors"
|
|
21
|
-
import { PluginDependencyManager } from "./dependency-manager"
|
|
22
|
-
import { readdir, readFile } from "fs/promises"
|
|
23
|
-
import { join, resolve, sep } from "path"
|
|
24
|
-
import { existsSync } from "fs"
|
|
25
|
-
|
|
26
|
-
export interface PluginRegistryConfig {
|
|
27
|
-
logger?: Logger
|
|
28
|
-
config?: FluxStackConfig
|
|
29
|
-
discoveryOptions?: PluginDiscoveryOptions
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export class PluginRegistry {
|
|
33
|
-
private plugins: Map<string, FluxStackPlugin> = new Map()
|
|
34
|
-
private manifests: Map<string, PluginManifest> = new Map()
|
|
35
|
-
private loadOrder: string[] = []
|
|
36
|
-
private dependencies: Map<string, string[]> = new Map()
|
|
37
|
-
private conflicts: string[] = []
|
|
38
|
-
private logger?: Logger
|
|
39
|
-
private config?: FluxStackConfig
|
|
40
|
-
private dependencyManager: PluginDependencyManager
|
|
41
|
-
|
|
42
|
-
constructor(options: PluginRegistryConfig = {}) {
|
|
43
|
-
this.logger = options.logger
|
|
44
|
-
this.config = options.config
|
|
45
|
-
this.dependencyManager = new PluginDependencyManager({
|
|
46
|
-
logger: this.logger,
|
|
47
|
-
autoInstall: true,
|
|
48
|
-
packageManager: 'bun'
|
|
49
|
-
})
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Register a plugin with the registry
|
|
54
|
-
*/
|
|
55
|
-
async register(plugin: FluxStackPlugin, manifest?: PluginManifest): Promise<void> {
|
|
56
|
-
if (this.plugins.has(plugin.name)) {
|
|
57
|
-
throw new FluxStackError(
|
|
58
|
-
`Plugin '${plugin.name}' is already registered`,
|
|
59
|
-
'PLUGIN_ALREADY_REGISTERED',
|
|
60
|
-
400
|
|
61
|
-
)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Validate plugin structure
|
|
65
|
-
this.validatePlugin(plugin)
|
|
66
|
-
|
|
67
|
-
// Validate plugin configuration if schema is provided
|
|
68
|
-
const pluginConfigs = this.config?.plugins.config as Record<string, unknown> | undefined
|
|
69
|
-
if (plugin.configSchema && pluginConfigs?.[plugin.name]) {
|
|
70
|
-
this.validatePluginConfig(plugin, pluginConfigs[plugin.name])
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
this.plugins.set(plugin.name, plugin)
|
|
74
|
-
|
|
75
|
-
if (manifest) {
|
|
76
|
-
this.manifests.set(plugin.name, manifest)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Update dependency tracking
|
|
80
|
-
if (plugin.dependencies) {
|
|
81
|
-
this.dependencies.set(plugin.name, plugin.dependencies)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Update load order
|
|
85
|
-
this.updateLoadOrder()
|
|
86
|
-
|
|
87
|
-
this.logger?.debug(`Plugin '${plugin.name}' registered successfully`, {
|
|
88
|
-
plugin: plugin.name,
|
|
89
|
-
version: plugin.version,
|
|
90
|
-
dependencies: plugin.dependencies
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
// Execute onPluginRegister hooks on all registered plugins
|
|
94
|
-
await this.executePluginRegisterHooks(plugin)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Execute onPluginRegister hooks on all plugins
|
|
99
|
-
*/
|
|
100
|
-
private async executePluginRegisterHooks(registeredPlugin: FluxStackPlugin): Promise<void> {
|
|
101
|
-
for (const plugin of this.plugins.values()) {
|
|
102
|
-
if (plugin.onPluginRegister && typeof plugin.onPluginRegister === 'function') {
|
|
103
|
-
try {
|
|
104
|
-
await plugin.onPluginRegister({
|
|
105
|
-
pluginName: registeredPlugin.name,
|
|
106
|
-
pluginVersion: registeredPlugin.version,
|
|
107
|
-
timestamp: Date.now(),
|
|
108
|
-
data: { plugin: registeredPlugin }
|
|
109
|
-
})
|
|
110
|
-
} catch (error) {
|
|
111
|
-
this.logger?.error(`Plugin '${plugin.name}' onPluginRegister hook failed`, {
|
|
112
|
-
error: error instanceof Error ? error.message : String(error)
|
|
113
|
-
})
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Execute onPluginUnregister hooks on all plugins
|
|
121
|
-
*/
|
|
122
|
-
private async executePluginUnregisterHooks(unregisteredPluginName: string, version?: string): Promise<void> {
|
|
123
|
-
for (const plugin of this.plugins.values()) {
|
|
124
|
-
if (plugin.onPluginUnregister && typeof plugin.onPluginUnregister === 'function') {
|
|
125
|
-
try {
|
|
126
|
-
await plugin.onPluginUnregister({
|
|
127
|
-
pluginName: unregisteredPluginName,
|
|
128
|
-
pluginVersion: version,
|
|
129
|
-
timestamp: Date.now()
|
|
130
|
-
})
|
|
131
|
-
} catch (error) {
|
|
132
|
-
this.logger?.error(`Plugin '${plugin.name}' onPluginUnregister hook failed`, {
|
|
133
|
-
error: error instanceof Error ? error.message : String(error)
|
|
134
|
-
})
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Unregister a plugin from the registry
|
|
142
|
-
*/
|
|
143
|
-
async unregister(name: string): Promise<void> {
|
|
144
|
-
if (!this.plugins.has(name)) {
|
|
145
|
-
throw new FluxStackError(
|
|
146
|
-
`Plugin '${name}' is not registered`,
|
|
147
|
-
'PLUGIN_NOT_FOUND',
|
|
148
|
-
404
|
|
149
|
-
)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Check if other plugins depend on this one
|
|
153
|
-
const dependents = this.getDependents(name)
|
|
154
|
-
if (dependents.length > 0) {
|
|
155
|
-
throw new FluxStackError(
|
|
156
|
-
`Cannot unregister plugin '${name}' because it is required by: ${dependents.join(', ')}`,
|
|
157
|
-
'PLUGIN_HAS_DEPENDENTS',
|
|
158
|
-
400
|
|
159
|
-
)
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const plugin = this.plugins.get(name)
|
|
163
|
-
const version = plugin?.version
|
|
164
|
-
|
|
165
|
-
this.plugins.delete(name)
|
|
166
|
-
this.manifests.delete(name)
|
|
167
|
-
this.dependencies.delete(name)
|
|
168
|
-
this.loadOrder = this.loadOrder.filter(pluginName => pluginName !== name)
|
|
169
|
-
|
|
170
|
-
this.logger?.debug(`Plugin '${name}' unregistered successfully`)
|
|
171
|
-
|
|
172
|
-
// Execute onPluginUnregister hooks on all remaining plugins
|
|
173
|
-
await this.executePluginUnregisterHooks(name, version)
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Get a plugin by name
|
|
178
|
-
*/
|
|
179
|
-
get(name: string): FluxStackPlugin | undefined {
|
|
180
|
-
return this.plugins.get(name)
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Get plugin manifest by name
|
|
185
|
-
*/
|
|
186
|
-
getManifest(name: string): PluginManifest | undefined {
|
|
187
|
-
return this.manifests.get(name)
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Get all registered plugins
|
|
192
|
-
*/
|
|
193
|
-
getAll(): FluxStackPlugin[] {
|
|
194
|
-
return Array.from(this.plugins.values())
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Get all plugin manifests
|
|
199
|
-
*/
|
|
200
|
-
getAllManifests(): PluginManifest[] {
|
|
201
|
-
return Array.from(this.manifests.values())
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Get plugins in load order
|
|
206
|
-
*/
|
|
207
|
-
getLoadOrder(): string[] {
|
|
208
|
-
return [...this.loadOrder]
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Get plugins that depend on the specified plugin
|
|
213
|
-
*/
|
|
214
|
-
getDependents(pluginName: string): string[] {
|
|
215
|
-
const dependents: string[] = []
|
|
216
|
-
|
|
217
|
-
for (const [name, deps] of this.dependencies.entries()) {
|
|
218
|
-
if (deps.includes(pluginName)) {
|
|
219
|
-
dependents.push(name)
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
return dependents
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Get plugin dependencies
|
|
228
|
-
*/
|
|
229
|
-
getDependencies(pluginName: string): string[] {
|
|
230
|
-
return this.dependencies.get(pluginName) || []
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Check if a plugin is registered
|
|
235
|
-
*/
|
|
236
|
-
has(name: string): boolean {
|
|
237
|
-
return this.plugins.has(name)
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Register a plugin synchronously (no async hooks).
|
|
242
|
-
*
|
|
243
|
-
* Used by the framework to add plugins via .use() and during automatic
|
|
244
|
-
* plugin discovery, where the full async register() flow (which fires
|
|
245
|
-
* onPluginRegister hooks) is not needed — setup hooks run later in start().
|
|
246
|
-
*/
|
|
247
|
-
registerSync(plugin: FluxStackPlugin): void {
|
|
248
|
-
if (this.plugins.has(plugin.name)) {
|
|
249
|
-
throw new FluxStackError(
|
|
250
|
-
`Plugin '${plugin.name}' is already registered`,
|
|
251
|
-
'PLUGIN_ALREADY_REGISTERED',
|
|
252
|
-
400
|
|
253
|
-
)
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
this.validatePlugin(plugin)
|
|
257
|
-
this.plugins.set(plugin.name, plugin)
|
|
258
|
-
|
|
259
|
-
if (plugin.dependencies) {
|
|
260
|
-
this.dependencies.set(plugin.name, plugin.dependencies)
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
this.updateLoadOrder()
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Refresh the load order.
|
|
268
|
-
*
|
|
269
|
-
* Falls back to insertion-order if the topological sort fails
|
|
270
|
-
* (e.g. unresolvable external dependency listed but not yet registered).
|
|
271
|
-
*/
|
|
272
|
-
refreshLoadOrder(): void {
|
|
273
|
-
try {
|
|
274
|
-
this.updateLoadOrder()
|
|
275
|
-
} catch {
|
|
276
|
-
this.loadOrder = Array.from(this.plugins.keys())
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Return a read-only snapshot of the internal plugin map.
|
|
282
|
-
*
|
|
283
|
-
* Allows the framework to iterate over registered plugins for dependency
|
|
284
|
-
* validation without reaching into private fields.
|
|
285
|
-
*/
|
|
286
|
-
getPluginsMap(): ReadonlyMap<string, FluxStackPlugin> {
|
|
287
|
-
return this.plugins
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Check which dependencies are missing from main package.json
|
|
292
|
-
*/
|
|
293
|
-
private checkMissingDependencies(pluginDeps: Record<string, string>): string[] {
|
|
294
|
-
try {
|
|
295
|
-
const mainPackageJsonPath = join(process.cwd(), 'package.json')
|
|
296
|
-
if (!existsSync(mainPackageJsonPath)) {
|
|
297
|
-
return Object.keys(pluginDeps)
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const mainPackageJson = JSON.parse(
|
|
301
|
-
require('fs').readFileSync(mainPackageJsonPath, 'utf-8')
|
|
302
|
-
)
|
|
303
|
-
|
|
304
|
-
const allDeps = {
|
|
305
|
-
...mainPackageJson.dependencies,
|
|
306
|
-
...mainPackageJson.devDependencies
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
return Object.keys(pluginDeps).filter(dep => !allDeps[dep])
|
|
310
|
-
} catch (error) {
|
|
311
|
-
// If we can't read package.json, assume all deps are missing
|
|
312
|
-
return Object.keys(pluginDeps)
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* 🔒 Check if a plugin is allowed to be loaded (whitelist check)
|
|
318
|
-
*
|
|
319
|
-
* @param pluginName - Name of the plugin (e.g., "fluxstack-plugin-auth", "@acme/fplugin-payments")
|
|
320
|
-
* @param isNpmPlugin - Whether this is an npm plugin (requires whitelist) or project plugin (trusted)
|
|
321
|
-
* @returns true if plugin is allowed, false otherwise
|
|
322
|
-
*/
|
|
323
|
-
/**
|
|
324
|
-
* Check if a plugin is allowed to be loaded (whitelist enforcement)
|
|
325
|
-
*
|
|
326
|
-
* Security model:
|
|
327
|
-
* - Project plugins (plugins/) are ALWAYS trusted (developer put them there)
|
|
328
|
-
* - NPM plugins (node_modules/) REQUIRE whitelist (supply chain protection)
|
|
329
|
-
*/
|
|
330
|
-
private isPluginAllowed(pluginName: string, source: 'npm' | 'project'): boolean {
|
|
331
|
-
const allowedPlugins = this.config?.plugins.allowedPlugins || []
|
|
332
|
-
|
|
333
|
-
// Project plugins are always trusted - developer explicitly added them
|
|
334
|
-
if (source === 'project') {
|
|
335
|
-
if (!this.config?.plugins.discoverProjectPlugins) {
|
|
336
|
-
this.logger?.debug(`Project plugin '${pluginName}' skipped: discovery disabled`)
|
|
337
|
-
return false
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// ✅ Project plugins bypass whitelist - they're trusted by design
|
|
341
|
-
this.logger?.debug(`Project plugin '${pluginName}' allowed (trusted source)`)
|
|
342
|
-
return true
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
if ((allowedPlugins as string[]).length === 0) {
|
|
346
|
-
this.logger?.warn(`NPM plugin '${pluginName}' blocked: No plugins in whitelist (PLUGINS_ALLOWED is empty)`)
|
|
347
|
-
return false
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
if (!(allowedPlugins as string[]).includes(pluginName)) {
|
|
351
|
-
this.logger?.warn(`NPM plugin '${pluginName}' blocked: Not in whitelist (PLUGINS_ALLOWED)`, {
|
|
352
|
-
pluginName,
|
|
353
|
-
allowedPlugins
|
|
354
|
-
})
|
|
355
|
-
return false
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
return true
|
|
359
|
-
}
|
|
360
|
-
/**
|
|
361
|
-
* Get registry statistics
|
|
362
|
-
*/
|
|
363
|
-
getStats() {
|
|
364
|
-
return {
|
|
365
|
-
totalPlugins: this.plugins.size,
|
|
366
|
-
enabledPlugins: (this.config?.plugins.enabled as string[] | undefined)?.length ?? 0,
|
|
367
|
-
disabledPlugins: (this.config?.plugins.disabled as string[] | undefined)?.length ?? 0,
|
|
368
|
-
conflicts: this.conflicts.length,
|
|
369
|
-
loadOrder: this.loadOrder.length
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* Validate all plugin dependencies
|
|
375
|
-
*/
|
|
376
|
-
validateDependencies(): void {
|
|
377
|
-
this.conflicts = []
|
|
378
|
-
|
|
379
|
-
for (const plugin of this.plugins.values()) {
|
|
380
|
-
if (plugin.dependencies) {
|
|
381
|
-
for (const dependency of plugin.dependencies) {
|
|
382
|
-
if (!this.plugins.has(dependency)) {
|
|
383
|
-
const error = `Plugin '${plugin.name}' depends on '${dependency}' which is not registered`
|
|
384
|
-
this.conflicts.push(error)
|
|
385
|
-
this.logger?.error(error, { plugin: plugin.name, dependency })
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
if (this.conflicts.length > 0) {
|
|
392
|
-
throw new FluxStackError(
|
|
393
|
-
`Plugin dependency validation failed: ${this.conflicts.join('; ')}`,
|
|
394
|
-
'PLUGIN_DEPENDENCY_ERROR',
|
|
395
|
-
400,
|
|
396
|
-
{ conflicts: this.conflicts }
|
|
397
|
-
)
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* Discover FluxStack plugins from node_modules
|
|
403
|
-
* Looks for packages with naming pattern:
|
|
404
|
-
* - fluxstack-plugin-*
|
|
405
|
-
* - fplugin-*
|
|
406
|
-
* - @fluxstack/plugin-*
|
|
407
|
-
* - @fplugin/*
|
|
408
|
-
* - @org/fluxstack-plugin-*
|
|
409
|
-
* - @org/fplugin-*
|
|
410
|
-
*
|
|
411
|
-
* 🔒 SECURITY: Respects config.plugins.discoverNpmPlugins and config.plugins.allowedPlugins
|
|
412
|
-
*/
|
|
413
|
-
async discoverNpmPlugins(): Promise<PluginLoadResult[]> {
|
|
414
|
-
const results: PluginLoadResult[] = []
|
|
415
|
-
const nodeModulesDir = 'node_modules'
|
|
416
|
-
|
|
417
|
-
// 🔒 Check if npm plugin discovery is enabled
|
|
418
|
-
if (!this.config?.plugins.discoverNpmPlugins) {
|
|
419
|
-
this.logger?.debug('NPM plugin discovery is disabled (PLUGINS_DISCOVER_NPM=false)')
|
|
420
|
-
return results
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
if (!existsSync(nodeModulesDir)) {
|
|
424
|
-
this.logger?.debug('node_modules directory not found')
|
|
425
|
-
return results
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
try {
|
|
429
|
-
const entries = await readdir(nodeModulesDir, { withFileTypes: true })
|
|
430
|
-
|
|
431
|
-
for (const entry of entries) {
|
|
432
|
-
if (entry.isDirectory()) {
|
|
433
|
-
// Check scoped packages (@org/package)
|
|
434
|
-
if (entry.name.startsWith('@')) {
|
|
435
|
-
const scopeDir = join(nodeModulesDir, entry.name)
|
|
436
|
-
const scopedEntries = await readdir(scopeDir, { withFileTypes: true })
|
|
437
|
-
|
|
438
|
-
for (const scopedEntry of scopedEntries) {
|
|
439
|
-
if (scopedEntry.isDirectory()) {
|
|
440
|
-
const packageName = `${entry.name}/${scopedEntry.name}`
|
|
441
|
-
let isFluxStackPlugin = false
|
|
442
|
-
|
|
443
|
-
// Match patterns:
|
|
444
|
-
// @fluxstack/plugin-*
|
|
445
|
-
if (entry.name === '@fluxstack' && scopedEntry.name.startsWith('plugin-')) {
|
|
446
|
-
isFluxStackPlugin = true
|
|
447
|
-
}
|
|
448
|
-
// @fplugin/*
|
|
449
|
-
else if (entry.name === '@fplugin') {
|
|
450
|
-
isFluxStackPlugin = true
|
|
451
|
-
}
|
|
452
|
-
// @org/fluxstack-plugin-*
|
|
453
|
-
else if (scopedEntry.name.startsWith('fluxstack-plugin-')) {
|
|
454
|
-
isFluxStackPlugin = true
|
|
455
|
-
}
|
|
456
|
-
// @org/fplugin-*
|
|
457
|
-
else if (scopedEntry.name.startsWith('fplugin-')) {
|
|
458
|
-
isFluxStackPlugin = true
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
if (isFluxStackPlugin) {
|
|
462
|
-
// 🔒 Security check: Verify plugin is in whitelist
|
|
463
|
-
if (!this.isPluginAllowed(packageName, 'npm')) {
|
|
464
|
-
this.logger?.debug(`Skipping npm plugin (not in whitelist): ${packageName}`)
|
|
465
|
-
results.push({
|
|
466
|
-
success: false,
|
|
467
|
-
error: `Plugin '${packageName}' is not in the allowed plugins whitelist (PLUGINS_ALLOWED)`
|
|
468
|
-
})
|
|
469
|
-
continue
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
const pluginPath = join(scopeDir, scopedEntry.name)
|
|
473
|
-
this.logger?.debug(`Loading whitelisted npm plugin: ${packageName}`)
|
|
474
|
-
|
|
475
|
-
const result = await this.loadPlugin(pluginPath)
|
|
476
|
-
results.push(result)
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
// Check non-scoped packages
|
|
482
|
-
else if (
|
|
483
|
-
entry.name.startsWith('fluxstack-plugin-') ||
|
|
484
|
-
entry.name.startsWith('fplugin-')
|
|
485
|
-
) {
|
|
486
|
-
// 🔒 Security check: Verify plugin is in whitelist
|
|
487
|
-
if (!this.isPluginAllowed(entry.name, 'npm')) {
|
|
488
|
-
this.logger?.debug(`Skipping npm plugin (not in whitelist): ${entry.name}`)
|
|
489
|
-
results.push({
|
|
490
|
-
success: false,
|
|
491
|
-
error: `Plugin '${entry.name}' is not in the allowed plugins whitelist (PLUGINS_ALLOWED)`
|
|
492
|
-
})
|
|
493
|
-
continue
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
const pluginPath = join(nodeModulesDir, entry.name)
|
|
497
|
-
this.logger?.debug(`Loading whitelisted npm plugin: ${entry.name}`)
|
|
498
|
-
|
|
499
|
-
const result = await this.loadPlugin(pluginPath)
|
|
500
|
-
results.push(result)
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
// 🔒 Security summary
|
|
506
|
-
const successful = results.filter(r => r.success).length
|
|
507
|
-
const blocked = results.filter(r => !r.success && r.error?.includes('whitelist')).length
|
|
508
|
-
const failed = results.filter(r => !r.success && !r.error?.includes('whitelist')).length
|
|
509
|
-
|
|
510
|
-
if (blocked > 0) {
|
|
511
|
-
this.logger?.warn(`🔒 Security: Blocked ${blocked} npm plugin(s) not in whitelist (PLUGINS_ALLOWED)`)
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
this.logger?.info(`Discovered ${successful} allowed npm plugin(s)`, {
|
|
515
|
-
total: results.length,
|
|
516
|
-
successful,
|
|
517
|
-
blocked,
|
|
518
|
-
failed
|
|
519
|
-
})
|
|
520
|
-
} catch (error) {
|
|
521
|
-
this.logger?.error('Failed to discover npm plugins', { error })
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
return results
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
/**
|
|
528
|
-
* Discover plugins from filesystem
|
|
529
|
-
*
|
|
530
|
-
* 🔒 SECURITY: Respects config.plugins.discoverProjectPlugins for project plugins
|
|
531
|
-
*/
|
|
532
|
-
async discoverPlugins(options: PluginDiscoveryOptions = {}): Promise<PluginLoadResult[]> {
|
|
533
|
-
const results: PluginLoadResult[] = []
|
|
534
|
-
const {
|
|
535
|
-
directories = ['plugins'],
|
|
536
|
-
patterns: _patterns = ['**/plugin.{js,ts}', '**/index.{js,ts}'],
|
|
537
|
-
includeBuiltIn: _includeBuiltIn = false,
|
|
538
|
-
includeExternal: _includeExternal = true
|
|
539
|
-
} = options
|
|
540
|
-
|
|
541
|
-
// 🔒 Check if project plugin discovery is enabled
|
|
542
|
-
if (!this.config?.plugins.discoverProjectPlugins) {
|
|
543
|
-
this.logger?.debug('Project plugin discovery is disabled (PLUGINS_DISCOVER_PROJECT=false)')
|
|
544
|
-
return results
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
// Descobrir plugins
|
|
548
|
-
for (const directory of directories) {
|
|
549
|
-
this.logger?.debug(`Scanning directory: ${directory}`)
|
|
550
|
-
if (!existsSync(directory)) {
|
|
551
|
-
this.logger?.warn(`Directory does not exist: ${directory}`)
|
|
552
|
-
continue
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
try {
|
|
556
|
-
const pluginResults = await this.discoverPluginsInDirectory(directory, _patterns)
|
|
557
|
-
this.logger?.debug(`Found ${pluginResults.length} plugins in ${directory}`)
|
|
558
|
-
|
|
559
|
-
for (const pluginResult of pluginResults) {
|
|
560
|
-
if (pluginResult.success && pluginResult.plugin) {
|
|
561
|
-
if (!this.isPluginAllowed(pluginResult.plugin.name, 'project')) {
|
|
562
|
-
results.push({
|
|
563
|
-
success: false,
|
|
564
|
-
error: `Plugin '${pluginResult.plugin.name}' is not in the allowed plugins whitelist (PLUGINS_ALLOWED)`
|
|
565
|
-
})
|
|
566
|
-
continue
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
results.push(pluginResult)
|
|
571
|
-
}
|
|
572
|
-
} catch (error) {
|
|
573
|
-
this.logger?.warn(`Failed to discover plugins in directory '${directory}'`, { error })
|
|
574
|
-
results.push({
|
|
575
|
-
success: false,
|
|
576
|
-
error: `Failed to scan directory: ${error instanceof Error ? error.message : String(error)}`
|
|
577
|
-
})
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
// Resolver e instalar dependências
|
|
582
|
-
await this.resolveDependencies(results)
|
|
583
|
-
|
|
584
|
-
return results
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
/**
|
|
588
|
-
* Load a plugin from file path
|
|
589
|
-
*/
|
|
590
|
-
async loadPlugin(pluginPath: string): Promise<PluginLoadResult> {
|
|
591
|
-
try {
|
|
592
|
-
// Check if manifest exists
|
|
593
|
-
const manifestPath = join(pluginPath, 'plugin.json')
|
|
594
|
-
let manifest: PluginManifest | undefined
|
|
595
|
-
|
|
596
|
-
if (existsSync(manifestPath)) {
|
|
597
|
-
const manifestContent = await readFile(manifestPath, 'utf-8')
|
|
598
|
-
manifest = JSON.parse(manifestContent)
|
|
599
|
-
} else {
|
|
600
|
-
// Try package.json for npm plugins
|
|
601
|
-
const packagePath = join(pluginPath, 'package.json')
|
|
602
|
-
if (existsSync(packagePath)) {
|
|
603
|
-
try {
|
|
604
|
-
const packageContent = await readFile(packagePath, 'utf-8')
|
|
605
|
-
const packageJson = JSON.parse(packageContent)
|
|
606
|
-
|
|
607
|
-
if (packageJson.fluxstack) {
|
|
608
|
-
manifest = {
|
|
609
|
-
name: packageJson.name,
|
|
610
|
-
version: packageJson.version,
|
|
611
|
-
description: packageJson.description || '',
|
|
612
|
-
author: packageJson.author || '',
|
|
613
|
-
license: packageJson.license || '',
|
|
614
|
-
homepage: packageJson.homepage,
|
|
615
|
-
repository: packageJson.repository,
|
|
616
|
-
keywords: packageJson.keywords || [],
|
|
617
|
-
dependencies: packageJson.dependencies || {},
|
|
618
|
-
peerDependencies: packageJson.peerDependencies,
|
|
619
|
-
fluxstack: packageJson.fluxstack
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
} catch (error) {
|
|
623
|
-
this.logger?.warn(`Failed to parse package.json in '${pluginPath}'`, { error })
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
// Check and install plugin dependencies
|
|
629
|
-
if (manifest && manifest.dependencies && Object.keys(manifest.dependencies).length > 0) {
|
|
630
|
-
const isProjectPlugin = pluginPath.includes('plugins' + sep)
|
|
631
|
-
|
|
632
|
-
if (isProjectPlugin) {
|
|
633
|
-
// Install dependencies locally in plugin directory
|
|
634
|
-
this.logger?.debug(
|
|
635
|
-
`Installing dependencies for plugin '${manifest.name}' in ${pluginPath}`,
|
|
636
|
-
{ dependencies: Object.keys(manifest.dependencies).length }
|
|
637
|
-
)
|
|
638
|
-
|
|
639
|
-
try {
|
|
640
|
-
await this.dependencyManager.installDependenciesInPath(
|
|
641
|
-
pluginPath,
|
|
642
|
-
manifest.dependencies
|
|
643
|
-
)
|
|
644
|
-
} catch (error) {
|
|
645
|
-
this.logger?.warn(
|
|
646
|
-
`Failed to install dependencies for plugin '${manifest.name}'. ` +
|
|
647
|
-
`You can install manually with: cd ${pluginPath} && bun install`
|
|
648
|
-
)
|
|
649
|
-
}
|
|
650
|
-
} else {
|
|
651
|
-
// NPM plugins always show warning
|
|
652
|
-
this.logger?.warn(`Plugin '${manifest.name}' declares dependencies. Run 'bun run flux plugin:deps install ${manifest.name}' to review and install them manually.`)
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
// Try to import the plugin (after dependencies are installed)
|
|
657
|
-
const pluginModule = await import(resolve(pluginPath))
|
|
658
|
-
const plugin: FluxStackPlugin = pluginModule.default || pluginModule
|
|
659
|
-
|
|
660
|
-
if (!plugin || typeof plugin !== 'object' || !plugin.name) {
|
|
661
|
-
return {
|
|
662
|
-
success: false,
|
|
663
|
-
error: 'Invalid plugin: must export a plugin object with a name property'
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
// Register the plugin
|
|
668
|
-
await this.register(plugin, manifest)
|
|
669
|
-
|
|
670
|
-
return {
|
|
671
|
-
success: true,
|
|
672
|
-
plugin,
|
|
673
|
-
warnings: manifest ? [] : ['No plugin manifest found']
|
|
674
|
-
}
|
|
675
|
-
} catch (error) {
|
|
676
|
-
return {
|
|
677
|
-
success: false,
|
|
678
|
-
error: error instanceof Error ? error.message : String(error)
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
/**
|
|
684
|
-
* Validate plugin structure
|
|
685
|
-
*/
|
|
686
|
-
private validatePlugin(plugin: FluxStackPlugin): void {
|
|
687
|
-
if (!plugin.name || typeof plugin.name !== 'string') {
|
|
688
|
-
throw new FluxStackError(
|
|
689
|
-
'Plugin must have a valid name property',
|
|
690
|
-
'INVALID_PLUGIN_STRUCTURE',
|
|
691
|
-
400
|
|
692
|
-
)
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
if (plugin.version && typeof plugin.version !== 'string') {
|
|
696
|
-
throw new FluxStackError(
|
|
697
|
-
'Plugin version must be a string',
|
|
698
|
-
'INVALID_PLUGIN_STRUCTURE',
|
|
699
|
-
400
|
|
700
|
-
)
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
if (plugin.dependencies && !Array.isArray(plugin.dependencies)) {
|
|
704
|
-
throw new FluxStackError(
|
|
705
|
-
'Plugin dependencies must be an array',
|
|
706
|
-
'INVALID_PLUGIN_STRUCTURE',
|
|
707
|
-
400
|
|
708
|
-
)
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
if (plugin.priority !== undefined
|
|
712
|
-
&& typeof plugin.priority !== 'number'
|
|
713
|
-
&& !(typeof plugin.priority === 'string' && plugin.priority in PRIORITY_MAP)) {
|
|
714
|
-
throw new FluxStackError(
|
|
715
|
-
`Plugin priority must be a number or one of: ${Object.keys(PRIORITY_MAP).join(', ')}`,
|
|
716
|
-
'INVALID_PLUGIN_STRUCTURE',
|
|
717
|
-
400
|
|
718
|
-
)
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
/**
|
|
723
|
-
* Validate plugin configuration against schema
|
|
724
|
-
*/
|
|
725
|
-
private validatePluginConfig(plugin: FluxStackPlugin, config: unknown): void {
|
|
726
|
-
if (!plugin.configSchema) {
|
|
727
|
-
return
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
// Basic validation - in a real implementation, you'd use a proper JSON schema validator
|
|
731
|
-
if (plugin.configSchema.required) {
|
|
732
|
-
for (const requiredField of plugin.configSchema.required) {
|
|
733
|
-
if (!(requiredField in (config as Record<string, unknown>))) {
|
|
734
|
-
throw new FluxStackError(
|
|
735
|
-
`Plugin '${plugin.name}' configuration missing required field: ${requiredField}`,
|
|
736
|
-
'INVALID_PLUGIN_CONFIG',
|
|
737
|
-
400
|
|
738
|
-
)
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
/**
|
|
745
|
-
* Update the load order based on dependencies and priorities.
|
|
746
|
-
*
|
|
747
|
-
* Uses a priority-aware topological sort: at each round, picks all plugins
|
|
748
|
-
* whose dependencies are already placed, then sorts that group by priority
|
|
749
|
-
* (highest first) before appending. This preserves dependency constraints
|
|
750
|
-
* while respecting priority within each dependency level.
|
|
751
|
-
*/
|
|
752
|
-
private updateLoadOrder(): void {
|
|
753
|
-
// First, detect circular dependencies via DFS
|
|
754
|
-
const visiting = new Set<string>()
|
|
755
|
-
const visited = new Set<string>()
|
|
756
|
-
|
|
757
|
-
const detectCycles = (pluginName: string) => {
|
|
758
|
-
if (visiting.has(pluginName)) {
|
|
759
|
-
throw new FluxStackError(
|
|
760
|
-
`Circular dependency detected involving plugin '${pluginName}'`,
|
|
761
|
-
'CIRCULAR_DEPENDENCY',
|
|
762
|
-
400
|
|
763
|
-
)
|
|
764
|
-
}
|
|
765
|
-
if (visited.has(pluginName)) return
|
|
766
|
-
|
|
767
|
-
visiting.add(pluginName)
|
|
768
|
-
const plugin = this.plugins.get(pluginName)
|
|
769
|
-
if (plugin?.dependencies) {
|
|
770
|
-
for (const dep of plugin.dependencies) {
|
|
771
|
-
if (this.plugins.has(dep)) {
|
|
772
|
-
detectCycles(dep)
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
visiting.delete(pluginName)
|
|
777
|
-
visited.add(pluginName)
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
for (const pluginName of this.plugins.keys()) {
|
|
781
|
-
detectCycles(pluginName)
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
// Kahn's algorithm with priority-aware group selection
|
|
785
|
-
const placed = new Set<string>()
|
|
786
|
-
const order: string[] = []
|
|
787
|
-
const remaining = new Set(this.plugins.keys())
|
|
788
|
-
|
|
789
|
-
while (remaining.size > 0) {
|
|
790
|
-
// Find all plugins whose dependencies are satisfied
|
|
791
|
-
const ready: string[] = []
|
|
792
|
-
for (const name of remaining) {
|
|
793
|
-
const plugin = this.plugins.get(name)
|
|
794
|
-
const deps = plugin?.dependencies ?? []
|
|
795
|
-
const allDepsPlaced = deps.every(d => !this.plugins.has(d) || placed.has(d))
|
|
796
|
-
if (allDepsPlaced) {
|
|
797
|
-
ready.push(name)
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
if (ready.length === 0) {
|
|
802
|
-
// Should not happen after cycle detection, but guard against it
|
|
803
|
-
break
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
// Sort ready plugins by priority (highest first)
|
|
807
|
-
ready.sort((a, b) => {
|
|
808
|
-
const pluginA = this.plugins.get(a)
|
|
809
|
-
const pluginB = this.plugins.get(b)
|
|
810
|
-
return normalizePriority(pluginB?.priority) - normalizePriority(pluginA?.priority)
|
|
811
|
-
})
|
|
812
|
-
|
|
813
|
-
for (const name of ready) {
|
|
814
|
-
order.push(name)
|
|
815
|
-
placed.add(name)
|
|
816
|
-
remaining.delete(name)
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
this.loadOrder = order
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
/**
|
|
824
|
-
* Resolver dependências de todos os plugins descobertos
|
|
825
|
-
*/
|
|
826
|
-
private async resolveDependencies(results: PluginLoadResult[]): Promise<void> {
|
|
827
|
-
// Dependencies are now installed during plugin loading in loadPlugin()
|
|
828
|
-
// This method is kept for compatibility but no longer performs installation
|
|
829
|
-
|
|
830
|
-
// Only check for dependency conflicts on successfully loaded plugins
|
|
831
|
-
for (const result of results) {
|
|
832
|
-
if (result.success && result.plugin) {
|
|
833
|
-
try {
|
|
834
|
-
const pluginDir = this.findPluginDirectory(result.plugin.name)
|
|
835
|
-
if (pluginDir) {
|
|
836
|
-
const resolution = await this.dependencyManager.resolvePluginDependencies(pluginDir)
|
|
837
|
-
|
|
838
|
-
if (!resolution.resolved) {
|
|
839
|
-
this.logger?.warn(`Plugin '${result.plugin.name}' has dependency conflicts`, {
|
|
840
|
-
conflicts: resolution.conflicts.length
|
|
841
|
-
})
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
} catch (error) {
|
|
845
|
-
this.logger?.warn(`Failed to check dependencies for plugin '${result.plugin.name}'`, { error })
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
/**
|
|
852
|
-
* Encontrar diretório de um plugin pelo nome
|
|
853
|
-
*/
|
|
854
|
-
private findPluginDirectory(pluginName: string): string | null {
|
|
855
|
-
const possiblePaths = [
|
|
856
|
-
`plugins/${pluginName}`,
|
|
857
|
-
`core/plugins/built-in/${pluginName}`
|
|
858
|
-
]
|
|
859
|
-
|
|
860
|
-
for (const path of possiblePaths) {
|
|
861
|
-
if (existsSync(path)) {
|
|
862
|
-
return path
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
return null
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
/**
|
|
870
|
-
* Discover plugins in a specific directory
|
|
871
|
-
*/
|
|
872
|
-
private async discoverPluginsInDirectory(
|
|
873
|
-
directory: string,
|
|
874
|
-
_patterns: string[]
|
|
875
|
-
): Promise<PluginLoadResult[]> {
|
|
876
|
-
const results: PluginLoadResult[] = []
|
|
877
|
-
|
|
878
|
-
try {
|
|
879
|
-
const entries = await readdir(directory, { withFileTypes: true })
|
|
880
|
-
|
|
881
|
-
for (const entry of entries) {
|
|
882
|
-
if (entry.isDirectory()) {
|
|
883
|
-
const pluginDir = join(directory, entry.name)
|
|
884
|
-
|
|
885
|
-
// Check if this looks like a plugin directory
|
|
886
|
-
// Skip if it's just an index file in the root of built-in directory
|
|
887
|
-
if (directory === 'core/plugins/built-in' && entry.name === 'index.ts') {
|
|
888
|
-
continue
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
const hasPluginFile = existsSync(join(pluginDir, 'index.ts')) ||
|
|
892
|
-
existsSync(join(pluginDir, 'index.js')) ||
|
|
893
|
-
existsSync(join(pluginDir, 'plugin.ts')) ||
|
|
894
|
-
existsSync(join(pluginDir, 'plugin.js'))
|
|
895
|
-
|
|
896
|
-
if (hasPluginFile) {
|
|
897
|
-
this.logger?.debug(`Loading plugin from: ${pluginDir}`)
|
|
898
|
-
const result = await this.loadPlugin(pluginDir)
|
|
899
|
-
results.push(result)
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
} catch (error) {
|
|
904
|
-
this.logger?.error(`Failed to read directory '${directory}'`, { error })
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
return results
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|