entkapp 5.4.0 → 5.6.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 (76) hide show
  1. package/README.md +7 -1
  2. package/bin/cli.js +117 -58
  3. package/bin/cli.mjs +175 -0
  4. package/index.cjs +18 -0
  5. package/index.mjs +51 -0
  6. package/package.json +7 -6
  7. package/src/EngineContext.js +0 -6
  8. package/src/EngineContext.mjs +428 -0
  9. package/src/Initializer.mjs +82 -0
  10. package/src/analyzers/CodeSmellAnalyzer.mjs +106 -0
  11. package/src/analyzers/OxcAnalyzer.mjs +11 -0
  12. package/src/api/HeadlessAPI.js +31 -16
  13. package/src/api/HeadlessAPI.mjs +369 -0
  14. package/src/api/PluginSDK.mjs +135 -0
  15. package/src/ast/ASTAnalyzer.js +23 -97
  16. package/src/ast/ASTAnalyzer.mjs +742 -0
  17. package/src/ast/AdvancedAnalysis.mjs +586 -0
  18. package/src/ast/BarrelParser.mjs +230 -0
  19. package/src/ast/DeadCodeDetector.js +7 -5
  20. package/src/ast/DeadCodeDetector.mjs +92 -0
  21. package/src/ast/MagicDetector.js +43 -23
  22. package/src/ast/MagicDetector.mjs +203 -0
  23. package/src/ast/OxcAnalyzer.js +27 -190
  24. package/src/ast/OxcAnalyzer.mjs +188 -0
  25. package/src/ast/SecretScanner.mjs +374 -0
  26. package/src/healing/GitSandbox.mjs +82 -0
  27. package/src/healing/SelfHealer.mjs +48 -0
  28. package/src/index.js +41 -88
  29. package/src/index.mjs +1176 -0
  30. package/src/performance/GraphCache.js +0 -27
  31. package/src/performance/GraphCache.mjs +108 -0
  32. package/src/performance/SupplyChainGuard.mjs +92 -0
  33. package/src/performance/WorkerPool.js +4 -22
  34. package/src/performance/WorkerPool.mjs +132 -0
  35. package/src/performance/WorkerTaskRunner.js +0 -26
  36. package/src/performance/WorkerTaskRunner.mjs +144 -0
  37. package/src/plugins/BasePlugin.js +27 -16
  38. package/src/plugins/BasePlugin.mjs +240 -0
  39. package/src/plugins/PluginRegistry.js +0 -44
  40. package/src/plugins/PluginRegistry.mjs +203 -0
  41. package/src/plugins/ecosystems/BackendServices.mjs +197 -0
  42. package/src/plugins/ecosystems/GenericPlugins.mjs +142 -0
  43. package/src/plugins/ecosystems/ModernFrameworks.mjs +162 -0
  44. package/src/plugins/ecosystems/MorePlugins.mjs +562 -0
  45. package/src/plugins/ecosystems/NewPlugins.mjs +526 -0
  46. package/src/plugins/ecosystems/NextJsPlugin.mjs +45 -0
  47. package/src/plugins/ecosystems/PluginLoader.mjs +193 -0
  48. package/src/plugins/ecosystems/TypeScriptPlugin.mjs +56 -0
  49. package/src/plugins/ecosystems/UltimateBundle.js +0 -259
  50. package/src/plugins/ecosystems/UltimateBundle.mjs +1182 -0
  51. package/src/refractor/ImpactAnalyzer.mjs +92 -0
  52. package/src/refractor/SourceRewriter.mjs +86 -0
  53. package/src/refractor/TransactionManager.mjs +5 -0
  54. package/src/refractor/TypeIntegrity.mjs +4 -0
  55. package/src/resolution/BuildOrchestrator.mjs +46 -0
  56. package/src/resolution/CircularDetector.mjs +91 -0
  57. package/src/resolution/ConfigGenerator.mjs +83 -0
  58. package/src/resolution/ConfigLoader.js +26 -190
  59. package/src/resolution/ConfigLoader.mjs +94 -0
  60. package/src/resolution/DepencyResolver.mjs +66 -0
  61. package/src/resolution/DependencyFixer.mjs +88 -0
  62. package/src/resolution/DependencyProfiler.mjs +286 -0
  63. package/src/resolution/EntryPointDetector.mjs +134 -0
  64. package/src/resolution/FrameworkConfigParser.mjs +119 -0
  65. package/src/resolution/GraphAnalyzer.js +1 -65
  66. package/src/resolution/GraphAnalyzer.mjs +80 -0
  67. package/src/resolution/MigrationAnalyzer.mjs +60 -0
  68. package/src/resolution/PathMapper.js +0 -81
  69. package/src/resolution/PathMapper.mjs +82 -0
  70. package/src/resolution/TSConfigLoader.js +1 -3
  71. package/src/resolution/TSConfigLoader.mjs +162 -0
  72. package/src/resolution/WorkSpaceGraph.js +89 -260
  73. package/src/resolution/WorkSpaceGraph.mjs +183 -0
  74. package/src/resolution/WorkspaceDiagnostic.mjs +139 -0
  75. package/test.js +76 -0
  76. package/wrangler.toml +6 -0
@@ -5,7 +5,7 @@ import path from 'path';
5
5
  * Base class for all entkapp plugins.
6
6
  * Defines the contract for ecosystem detection, entry point mapping,
7
7
  * and missing dependency / devDependency detection.
8
- * Version 5.4.0: Added detectEntryPoints() for dynamic framework entry detection.
8
+ * Version 5.0.0: Added getMissingDependencies(), getOrphanedConfigs(), runDependencyDiagnostics().
9
9
  */
10
10
  export class BasePlugin {
11
11
  constructor(context) {
@@ -34,17 +34,6 @@ export class BasePlugin {
34
34
  return [];
35
35
  }
36
36
 
37
- /**
38
- * Version 5.4.0: Dynamically detect entry points from a file's content.
39
- * Useful for parsing vite.config.ts, webpack.config.js, etc.
40
- * @param {string} content - The file content
41
- * @param {string} filePath - The absolute path to the file
42
- * @returns {Array<string>} List of relative or absolute entry point paths
43
- */
44
- detectEntryPoints(content, filePath) {
45
- return [];
46
- }
47
-
48
37
  /**
49
38
  * Returns symbols that are implicitly required/exported by the framework.
50
39
  */
@@ -54,13 +43,25 @@ export class BasePlugin {
54
43
 
55
44
  /**
56
45
  * Returns the npm package names required for this plugin to work.
46
+ * Each entry is either a string or an object:
47
+ * { name: string, dev?: boolean, optional?: boolean }
48
+ * - dev: true => expected in devDependencies
49
+ * - dev: false => expected in dependencies
50
+ * - optional => only warn, not error
51
+ *
52
+ * Override in subclasses to enable automatic dependency checking.
53
+ * @returns {Array<string | {name: string, dev?: boolean, optional?: boolean}>}
57
54
  */
58
55
  getRequiredPackages() {
59
56
  return [];
60
57
  }
61
58
 
62
59
  /**
63
- * Checks whether all required packages are present in package.json.
60
+ * Version 5.0.0: Checks whether all required packages are present in package.json.
61
+ * Returns an array of diagnostic objects describing missing or misplaced dependencies.
62
+ *
63
+ * @param {string} baseDir - The project root directory
64
+ * @returns {Promise<Array>}
64
65
  */
65
66
  async getMissingDependencies(baseDir) {
66
67
  const diagnostics = [];
@@ -146,7 +147,11 @@ export class BasePlugin {
146
147
  }
147
148
 
148
149
  /**
149
- * Checks for config files that exist without the corresponding package.
150
+ * Version 5.0.0: Checks for config files that exist without the corresponding package.
151
+ * Detects "orphaned" configs – e.g. .prettierrc exists but prettier is not installed.
152
+ *
153
+ * @param {string} baseDir
154
+ * @returns {Promise<Array>}
150
155
  */
151
156
  async getOrphanedConfigs(baseDir) {
152
157
  const diagnostics = [];
@@ -190,7 +195,11 @@ export class BasePlugin {
190
195
  }
191
196
 
192
197
  /**
193
- * Run all dependency diagnostics (missing + orphaned).
198
+ * Version 5.0.0: Run all dependency diagnostics (missing + orphaned).
199
+ * Convenience method combining getMissingDependencies and getOrphanedConfigs.
200
+ *
201
+ * @param {string} baseDir
202
+ * @returns {Promise<Array>}
194
203
  */
195
204
  async runDependencyDiagnostics(baseDir) {
196
205
  const [missing, orphaned] = await Promise.all([
@@ -201,7 +210,9 @@ export class BasePlugin {
201
210
  }
202
211
 
203
212
  /**
204
- * Dynamic getter for custom plugin properties.
213
+ * Version 3.2.0: Dynamic getter for custom plugin properties.
214
+ * @param {string} key - The property key to retrieve
215
+ * @returns {any} The value of the custom property
205
216
  */
206
217
  get(key) {
207
218
  const methodName = `get${key.charAt(0).toUpperCase() + key.slice(1)}`;
@@ -0,0 +1,240 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+
4
+ /**
5
+ * Base class for all entkapp plugins.
6
+ * Defines the contract for ecosystem detection, entry point mapping,
7
+ * and missing dependency / devDependency detection.
8
+ * Version 5.0.0: Added getMissingDependencies(), getOrphanedConfigs(), runDependencyDiagnostics().
9
+ */
10
+ export class BasePlugin {
11
+ constructor(context) {
12
+ this.context = context;
13
+ this.customGetters = new Map();
14
+ }
15
+
16
+ /**
17
+ * Unique identifier for the plugin (e.g., 'nextjs').
18
+ */
19
+ get name() {
20
+ throw new Error('Plugin must implement name getter');
21
+ }
22
+
23
+ /**
24
+ * Returns a list of configuration files that indicate this ecosystem is active.
25
+ */
26
+ getConfigFiles() {
27
+ return [];
28
+ }
29
+
30
+ /**
31
+ * Returns regex patterns for files that should be treated as entry points.
32
+ */
33
+ getRoutePatterns() {
34
+ return [];
35
+ }
36
+
37
+ /**
38
+ * Returns symbols that are implicitly required/exported by the framework.
39
+ */
40
+ getRequiredSystemContracts() {
41
+ return ['default'];
42
+ }
43
+
44
+ /**
45
+ * Returns the npm package names required for this plugin to work.
46
+ * Each entry is either a string or an object:
47
+ * { name: string, dev?: boolean, optional?: boolean }
48
+ * - dev: true => expected in devDependencies
49
+ * - dev: false => expected in dependencies
50
+ * - optional => only warn, not error
51
+ *
52
+ * Override in subclasses to enable automatic dependency checking.
53
+ * @returns {Array<string | {name: string, dev?: boolean, optional?: boolean}>}
54
+ */
55
+ getRequiredPackages() {
56
+ return [];
57
+ }
58
+
59
+ /**
60
+ * Version 5.0.0: Checks whether all required packages are present in package.json.
61
+ * Returns an array of diagnostic objects describing missing or misplaced dependencies.
62
+ *
63
+ * @param {string} baseDir - The project root directory
64
+ * @returns {Promise<Array>}
65
+ */
66
+ async getMissingDependencies(baseDir) {
67
+ const diagnostics = [];
68
+ const requiredPackages = this.getRequiredPackages();
69
+ if (requiredPackages.length === 0) return diagnostics;
70
+
71
+ let pkg = null;
72
+ try {
73
+ const raw = await fs.readFile(path.join(baseDir, 'package.json'), 'utf8');
74
+ pkg = JSON.parse(raw);
75
+ } catch {
76
+ return diagnostics;
77
+ }
78
+
79
+ const deps = pkg.dependencies || {};
80
+ const devDeps = pkg.devDependencies || {};
81
+
82
+ for (const entry of requiredPackages) {
83
+ const pkgName = typeof entry === 'string' ? entry : entry.name;
84
+ const isDev = typeof entry === 'object' ? entry.dev : undefined;
85
+ const isOptional = typeof entry === 'object' ? (entry.optional ?? false) : false;
86
+
87
+ const inDeps = pkgName in deps;
88
+ const inDevDeps = pkgName in devDeps;
89
+ const found = inDeps ? 'dependencies' : inDevDeps ? 'devDependencies' : null;
90
+
91
+ if (!found) {
92
+ let triggerFile = null;
93
+ for (const cfgFile of this.getConfigFiles()) {
94
+ try {
95
+ await fs.access(path.join(baseDir, cfgFile));
96
+ triggerFile = cfgFile;
97
+ break;
98
+ } catch { /* not found */ }
99
+ }
100
+
101
+ const expectedIn = isDev === true ? 'devDependencies' : isDev === false ? 'dependencies' : 'either';
102
+ const severity = isOptional ? 'warning' : 'error';
103
+
104
+ let message = `Plugin "${this.name}": package "${pkgName}" is not installed`;
105
+ if (triggerFile) {
106
+ message += ` (config file "${triggerFile}" was found)`;
107
+ }
108
+ if (isDev === true) {
109
+ message += ` — expected in devDependencies`;
110
+ } else if (isDev === false) {
111
+ message += ` — expected in dependencies`;
112
+ }
113
+
114
+ diagnostics.push({
115
+ plugin: this.name,
116
+ package: pkgName,
117
+ expectedIn,
118
+ foundIn: null,
119
+ configFile: triggerFile,
120
+ severity,
121
+ message,
122
+ });
123
+ } else if (isDev === true && found === 'dependencies') {
124
+ diagnostics.push({
125
+ plugin: this.name,
126
+ package: pkgName,
127
+ expectedIn: 'devDependencies',
128
+ foundIn: 'dependencies',
129
+ configFile: null,
130
+ severity: 'warning',
131
+ message: `Plugin "${this.name}": package "${pkgName}" is in "dependencies" but should be in "devDependencies"`,
132
+ });
133
+ } else if (isDev === false && found === 'devDependencies') {
134
+ diagnostics.push({
135
+ plugin: this.name,
136
+ package: pkgName,
137
+ expectedIn: 'dependencies',
138
+ foundIn: 'devDependencies',
139
+ configFile: null,
140
+ severity: 'warning',
141
+ message: `Plugin "${this.name}": package "${pkgName}" is in "devDependencies" but should be in "dependencies"`,
142
+ });
143
+ }
144
+ }
145
+
146
+ return diagnostics;
147
+ }
148
+
149
+ /**
150
+ * Version 5.0.0: Checks for config files that exist without the corresponding package.
151
+ * Detects "orphaned" configs – e.g. .prettierrc exists but prettier is not installed.
152
+ *
153
+ * @param {string} baseDir
154
+ * @returns {Promise<Array>}
155
+ */
156
+ async getOrphanedConfigs(baseDir) {
157
+ const diagnostics = [];
158
+ const requiredPackages = this.getRequiredPackages();
159
+ if (requiredPackages.length === 0) return diagnostics;
160
+
161
+ let pkg = null;
162
+ try {
163
+ const raw = await fs.readFile(path.join(baseDir, 'package.json'), 'utf8');
164
+ pkg = JSON.parse(raw);
165
+ } catch {
166
+ return diagnostics;
167
+ }
168
+
169
+ const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
170
+
171
+ const anyPackagePresent = requiredPackages.some(entry => {
172
+ const pkgName = typeof entry === 'string' ? entry : entry.name;
173
+ return pkgName in allDeps;
174
+ });
175
+
176
+ if (!anyPackagePresent) {
177
+ for (const cfgFile of this.getConfigFiles()) {
178
+ try {
179
+ await fs.access(path.join(baseDir, cfgFile));
180
+ const missingPkgs = requiredPackages
181
+ .map(e => typeof e === 'string' ? e : e.name)
182
+ .filter(n => !(n in allDeps));
183
+
184
+ diagnostics.push({
185
+ plugin: this.name,
186
+ configFile: cfgFile,
187
+ severity: 'error',
188
+ message: `Plugin "${this.name}": config file "${cfgFile}" exists but required package(s) [${missingPkgs.join(', ')}] are not installed`,
189
+ });
190
+ } catch { /* file not found */ }
191
+ }
192
+ }
193
+
194
+ return diagnostics;
195
+ }
196
+
197
+ /**
198
+ * Version 5.0.0: Run all dependency diagnostics (missing + orphaned).
199
+ * Convenience method combining getMissingDependencies and getOrphanedConfigs.
200
+ *
201
+ * @param {string} baseDir
202
+ * @returns {Promise<Array>}
203
+ */
204
+ async runDependencyDiagnostics(baseDir) {
205
+ const [missing, orphaned] = await Promise.all([
206
+ this.getMissingDependencies(baseDir),
207
+ this.getOrphanedConfigs(baseDir),
208
+ ]);
209
+ return [...missing, ...orphaned];
210
+ }
211
+
212
+ /**
213
+ * Version 3.2.0: Dynamic getter for custom plugin properties.
214
+ * @param {string} key - The property key to retrieve
215
+ * @returns {any} The value of the custom property
216
+ */
217
+ get(key) {
218
+ const methodName = `get${key.charAt(0).toUpperCase() + key.slice(1)}`;
219
+ if (typeof this[methodName] === 'function') {
220
+ return this[methodName]();
221
+ }
222
+ return this.customGetters.get(key);
223
+ }
224
+
225
+ /**
226
+ * Optional: Logic to detect if the plugin should be active in the given directory.
227
+ */
228
+ async isActive(baseDir) {
229
+ const configFiles = this.getConfigFiles();
230
+ for (const file of configFiles) {
231
+ try {
232
+ await fs.access(path.join(baseDir, file));
233
+ return true;
234
+ } catch {
235
+ continue;
236
+ }
237
+ }
238
+ return false;
239
+ }
240
+ }
@@ -111,31 +111,6 @@ export class PluginRegistry {
111
111
  return this.plugins.get(name);
112
112
  }
113
113
 
114
- /**
115
- * Version 5.4.0: Collect entry points from all plugins for a given file.
116
- * @param {string} content - The file content
117
- * @param {string} filePath - The absolute path to the file
118
- * @returns {Array<string>} List of detected entry points
119
- */
120
- detectEntryPointsFromContent(content, filePath) {
121
- const entryPoints = [];
122
- for (const plugin of this.plugins.values()) {
123
- if (typeof plugin.detectEntryPoints === 'function') {
124
- try {
125
- const detected = plugin.detectEntryPoints(content, filePath);
126
- if (Array.isArray(detected)) {
127
- entryPoints.push(...detected);
128
- }
129
- } catch (e) {
130
- if (this.context?.verbose) {
131
- console.warn(`[PluginRegistry] Entry detection failed for plugin "${plugin.name}" on ${filePath}:`, e.message);
132
- }
133
- }
134
- }
135
- }
136
- return entryPoints;
137
- }
138
-
139
114
  async getActivePlugins(baseDir) {
140
115
  const active = [];
141
116
  for (const plugin of this.plugins.values()) {
@@ -146,25 +121,6 @@ export class PluginRegistry {
146
121
  return active;
147
122
  }
148
123
 
149
- /**
150
- * Version 5.3.0: Run content analysis on a node using all active plugins.
151
- * @param {GraphNode} node - The node to analyze
152
- * @param {string} filePath - The absolute path to the file
153
- */
154
- async runPluginAnalysis(node, filePath) {
155
- for (const plugin of this.plugins.values()) {
156
- if (typeof plugin.analyze === 'function') {
157
- try {
158
- await plugin.analyze(node, filePath);
159
- } catch (e) {
160
- if (this.context?.verbose) {
161
- console.warn(`[PluginRegistry] Analysis failed for plugin "${plugin.name}" on ${filePath}:`, e.message);
162
- }
163
- }
164
- }
165
- }
166
- }
167
-
168
124
  /**
169
125
  * Version 5.0.0: Run dependency diagnostics across all registered plugins.
170
126
  * Returns a consolidated list of missing/misplaced dependencies and orphaned configs.
@@ -0,0 +1,203 @@
1
+ import { loadAdditionalPlugins } from "./ecosystems/PluginLoader.mjs";
2
+ import path from 'path';
3
+ import fs from 'fs/promises';
4
+ import { pathToFileURL } from 'url';
5
+ import { AllPluginClasses } from './ecosystems/UltimateBundle.mjs';
6
+ /**
7
+ * ============================================================================
8
+ * Plugin Registry for entkapp v5.0.0
9
+ * ============================================================================
10
+ * Advanced Plugin Registry supporting Builtin and Custom plugins.
11
+ * v5.0.0: Added runAllDependencyDiagnostics() for project-wide dependency checks.
12
+ */
13
+ export class PluginRegistry {
14
+ constructor(context) {
15
+ this.context = context;
16
+ this.plugins = new Map();
17
+ this.config = null;
18
+ }
19
+
20
+ async init(projectRoot) {
21
+ const configPath = path.join(projectRoot, 'entkapp', 'config.json');
22
+ try {
23
+ const configRaw = await fs.readFile(configPath, 'utf8');
24
+ this.config = JSON.parse(configRaw);
25
+ } catch (e) {
26
+ this.config = {
27
+ useBuiltinPlugins: true,
28
+ useCustomPlugins: true,
29
+ };
30
+ }
31
+ if (this.config.useBuiltinPlugins !== false) {
32
+ await this.loadBuiltinPlugins();
33
+ }
34
+ if (this.config.useCustomPlugins !== false) {
35
+ await this.loadCustomPlugins(projectRoot);
36
+ }
37
+ }
38
+
39
+ async loadBuiltinPlugins() {
40
+ // Delegate to PluginLoader which registers all built-in plugins
41
+ loadAdditionalPlugins(this);
42
+
43
+ // Also load TypeScript and Next.js plugins (primary framework plugins)
44
+
45
+
46
+ AllPluginClasses.forEach(PluginClass => {
47
+ this.register(new PluginClass(this.context));
48
+ });
49
+ }
50
+ async loadCustomPlugins(projectRoot) {
51
+ const pluginsDir = path.join(projectRoot, 'entkapp', 'plugins');
52
+ try {
53
+ const entries = await fs.readdir(pluginsDir, { withFileTypes: true });
54
+ for (const entry of entries) {
55
+ let pluginPath = path.join(pluginsDir, entry.name);
56
+ // Folder-based plugin support
57
+ if (entry.isDirectory()) {
58
+ const files = await fs.readdir(pluginPath);
59
+ if (files.includes('index.ts')) {
60
+ pluginPath = path.join(pluginPath, 'index.ts');
61
+ } else if (files.includes('index.mjs')) {
62
+ pluginPath = path.join(pluginPath, 'index.mjs');
63
+ } else {
64
+ continue;
65
+ }
66
+ }
67
+ if (pluginPath.endsWith('.ts')) {
68
+ await this.loadTypeScriptPlugin(pluginPath);
69
+ } else if (pluginPath.endsWith('.mjs') || pluginPath.endsWith('.mjs')) {
70
+ await this.loadJavaScriptPlugin(pluginPath);
71
+ }
72
+ }
73
+ } catch (e) {
74
+ // No custom plugins or dir missing
75
+ }
76
+ }
77
+
78
+ async loadJavaScriptPlugin(pluginPath) {
79
+ try {
80
+ const pluginModule = await import(pathToFileURL(pluginPath).href);
81
+ const PluginClass = pluginModule.default || pluginModule;
82
+ const pluginInstance = new PluginClass(this.context);
83
+ this.register(pluginInstance);
84
+ } catch (e) {
85
+ console.error(`[PluginRegistry] Failed to load JS plugin ${pluginPath}:`, e);
86
+ }
87
+ }
88
+
89
+ async loadTypeScriptPlugin(pluginPath) {
90
+ try {
91
+ if (this.context?.verbose) {
92
+ console.log(`[PluginRegistry] Transpiling TS plugin: ${pluginPath}`);
93
+ }
94
+ await this.loadJavaScriptPlugin(pluginPath);
95
+ } catch (e) {
96
+ console.error(`[PluginRegistry] Failed to load TS plugin ${pluginPath}:`, e);
97
+ }
98
+ }
99
+
100
+ register(plugin) {
101
+ if (plugin && plugin.name) {
102
+ this.plugins.set(plugin.name, plugin);
103
+ }
104
+ }
105
+
106
+ getPlugins() {
107
+ return Array.from(this.plugins.values());
108
+ }
109
+
110
+ getPlugin(name) {
111
+ return this.plugins.get(name);
112
+ }
113
+
114
+ async getActivePlugins(baseDir) {
115
+ const active = [];
116
+ for (const plugin of this.plugins.values()) {
117
+ if (await plugin.isActive(baseDir)) {
118
+ active.push(plugin);
119
+ }
120
+ }
121
+ return active;
122
+ }
123
+
124
+ /**
125
+ * Version 5.0.0: Run dependency diagnostics across all registered plugins.
126
+ * Returns a consolidated list of missing/misplaced dependencies and orphaned configs.
127
+ *
128
+ * @param {string} baseDir - The project root directory to check
129
+ * @returns {Promise<Array<{
130
+ * plugin: string,
131
+ * severity: 'error' | 'warning',
132
+ * message: string,
133
+ * package?: string,
134
+ * configFile?: string,
135
+ * expectedIn?: string,
136
+ * foundIn?: string | null
137
+ * }>>}
138
+ */
139
+ async runAllDependencyDiagnostics(baseDir) {
140
+ const allDiagnostics = [];
141
+ const seenMessages = new Set();
142
+
143
+ for (const plugin of this.plugins.values()) {
144
+ if (typeof plugin.runDependencyDiagnostics === 'function') {
145
+ try {
146
+ const diagnostics = await plugin.runDependencyDiagnostics(baseDir);
147
+ for (const d of diagnostics) {
148
+ // Deduplicate by message to avoid noise from overlapping plugins
149
+ if (!seenMessages.has(d.message)) {
150
+ seenMessages.add(d.message);
151
+ allDiagnostics.push(d);
152
+ }
153
+ }
154
+ } catch (e) {
155
+ if (this.context?.verbose) {
156
+ console.warn(`[PluginRegistry] Dependency diagnostic failed for plugin "${plugin.name}":`, e.message);
157
+ }
158
+ }
159
+ }
160
+ }
161
+
162
+ // Sort: errors first, then warnings; then alphabetically by plugin name
163
+ return allDiagnostics.sort((a, b) => {
164
+ if (a.severity !== b.severity) return a.severity === 'error' ? -1 : 1;
165
+ return a.plugin.localeCompare(b.plugin);
166
+ });
167
+ }
168
+
169
+ /**
170
+ * Version 5.0.0: Format dependency diagnostics for console output.
171
+ * @param {Array} diagnostics - Result of runAllDependencyDiagnostics()
172
+ * @returns {string} Formatted string ready for console output
173
+ */
174
+ formatDependencyDiagnostics(diagnostics) {
175
+ if (diagnostics.length === 0) {
176
+ return '[entkapp] No missing dependencies detected.';
177
+ }
178
+
179
+ const errors = diagnostics.filter(d => d.severity === 'error');
180
+ const warnings = diagnostics.filter(d => d.severity === 'warning');
181
+
182
+ const lines = [];
183
+ lines.push(`[entkapp] Dependency Diagnostics: ${errors.length} error(s), ${warnings.length} warning(s)`);
184
+ lines.push('');
185
+
186
+ if (errors.length > 0) {
187
+ lines.push(' Errors:');
188
+ for (const d of errors) {
189
+ lines.push(` [ERROR] ${d.message}`);
190
+ }
191
+ lines.push('');
192
+ }
193
+
194
+ if (warnings.length > 0) {
195
+ lines.push(' Warnings:');
196
+ for (const d of warnings) {
197
+ lines.push(` [WARN] ${d.message}`);
198
+ }
199
+ }
200
+
201
+ return lines.join('\n');
202
+ }
203
+ }