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.
Files changed (69) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/LLMD/INDEX.md +1 -1
  3. package/LLMD/MAINTENANCE.md +197 -197
  4. package/LLMD/MIGRATION.md +44 -1
  5. package/LLMD/agent.md +20 -7
  6. package/LLMD/config/declarative-system.md +268 -268
  7. package/LLMD/config/environment-vars.md +3 -6
  8. package/LLMD/config/runtime-reload.md +401 -401
  9. package/LLMD/core/build-system.md +599 -599
  10. package/LLMD/core/framework-lifecycle.md +249 -229
  11. package/LLMD/core/plugin-system.md +154 -100
  12. package/LLMD/patterns/anti-patterns.md +397 -397
  13. package/LLMD/patterns/project-structure.md +264 -264
  14. package/LLMD/patterns/type-safety.md +61 -5
  15. package/LLMD/reference/cli-commands.md +31 -7
  16. package/LLMD/reference/plugin-hooks.md +4 -2
  17. package/LLMD/reference/troubleshooting.md +364 -364
  18. package/LLMD/resources/controllers.md +465 -465
  19. package/LLMD/resources/live-auth.md +178 -1
  20. package/LLMD/resources/live-binary-delta.md +3 -1
  21. package/LLMD/resources/live-components.md +1192 -1041
  22. package/LLMD/resources/live-logging.md +3 -1
  23. package/LLMD/resources/live-rooms.md +1 -1
  24. package/LLMD/resources/live-upload.md +228 -181
  25. package/LLMD/resources/plugins-external.md +8 -7
  26. package/LLMD/resources/rest-auth.md +290 -290
  27. package/LLMD/resources/routes-eden.md +254 -254
  28. package/app/client/src/App.tsx +7 -7
  29. package/app/client/src/components/AppLayout.tsx +60 -23
  30. package/app/client/src/components/ColorWheel.tsx +195 -0
  31. package/app/client/src/components/DemoPage.tsx +5 -3
  32. package/app/client/src/components/LiveUploadWidget.tsx +1 -1
  33. package/app/client/src/components/ThemePicker.tsx +307 -0
  34. package/app/client/src/config/theme.config.ts +127 -0
  35. package/app/client/src/hooks/useThemeClock.ts +66 -0
  36. package/app/client/src/index.css +193 -0
  37. package/app/client/src/lib/theme-clock.ts +201 -0
  38. package/app/client/src/live/AuthDemo.tsx +9 -9
  39. package/app/client/src/live/CounterDemo.tsx +10 -10
  40. package/app/client/src/live/FormDemo.tsx +8 -8
  41. package/app/client/src/live/PingPongDemo.tsx +10 -10
  42. package/app/client/src/live/RoomChatDemo.tsx +10 -10
  43. package/app/client/src/live/SharedCounterDemo.tsx +5 -5
  44. package/app/client/src/pages/ApiTestPage.tsx +5 -5
  45. package/app/client/src/pages/HomePage.tsx +12 -12
  46. package/app/server/index.ts +8 -0
  47. package/app/server/live/auto-generated-components.ts +1 -1
  48. package/core/build/index.ts +1 -1
  49. package/core/cli/command-registry.ts +1 -1
  50. package/core/cli/commands/build.ts +25 -6
  51. package/core/cli/commands/plugin-deps.ts +1 -2
  52. package/core/cli/generators/plugin.ts +433 -581
  53. package/core/framework/server.ts +22 -8
  54. package/core/index.ts +6 -5
  55. package/core/plugins/index.ts +71 -199
  56. package/core/plugins/types.ts +76 -461
  57. package/core/server/index.ts +1 -1
  58. package/core/utils/logger/startup-banner.ts +26 -4
  59. package/create-fluxstack.ts +216 -107
  60. package/package.json +108 -107
  61. package/tsconfig.json +2 -1
  62. package/core/plugins/config.ts +0 -356
  63. package/core/plugins/dependency-manager.ts +0 -481
  64. package/core/plugins/discovery.ts +0 -379
  65. package/core/plugins/executor.ts +0 -353
  66. package/core/plugins/manager.ts +0 -645
  67. package/core/plugins/module-resolver.ts +0 -227
  68. package/core/plugins/registry.ts +0 -913
  69. package/vitest.config.live.ts +0 -69
@@ -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
-