entkapp 4.1.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 (46) hide show
  1. package/.scaffold-ignore +22 -0
  2. package/LICENSE +211 -0
  3. package/NOTICE +13 -0
  4. package/README.md +33 -0
  5. package/bin/cli.js +174 -0
  6. package/entkapp/config.json +42 -0
  7. package/entkapp/plugins/README.md +19 -0
  8. package/index.js +2254 -0
  9. package/logo.png +0 -0
  10. package/package.json +96 -0
  11. package/src/EngineContext.js +185 -0
  12. package/src/api/HeadlessAPI.js +376 -0
  13. package/src/api/PluginSDK.js +299 -0
  14. package/src/ast/ASTAnalyzer.js +492 -0
  15. package/src/ast/BarrelParser.js +221 -0
  16. package/src/ast/DeadCodeDetector.js +73 -0
  17. package/src/ast/MagicDetector.js +203 -0
  18. package/src/ast/OxcAnalyzer.js +337 -0
  19. package/src/ast/SecretScanner.js +304 -0
  20. package/src/healing/GitSandbox.js +82 -0
  21. package/src/healing/SelfHealer.js +52 -0
  22. package/src/index.js +723 -0
  23. package/src/performance/GraphCache.js +87 -0
  24. package/src/performance/SupplyChainGuard.js +92 -0
  25. package/src/performance/WorkerPool.js +109 -0
  26. package/src/plugins/BasePlugin.js +71 -0
  27. package/src/plugins/KnipAdapter.js +106 -0
  28. package/src/plugins/PluginRegistry.js +197 -0
  29. package/src/plugins/ecosystems/BackendServices.js +61 -0
  30. package/src/plugins/ecosystems/GenericPlugins.js +64 -0
  31. package/src/plugins/ecosystems/ModernFrameworks.js +159 -0
  32. package/src/plugins/ecosystems/MorePlugins.js +184 -0
  33. package/src/plugins/ecosystems/NextJsPlugin.js +33 -0
  34. package/src/plugins/ecosystems/PluginLoader.js +20 -0
  35. package/src/plugins/ecosystems/TypeScriptPlugin.js +56 -0
  36. package/src/refractor/ImpactAnalyzer.js +92 -0
  37. package/src/refractor/SourceRewriter.js +86 -0
  38. package/src/refractor/TransactionManager.js +138 -0
  39. package/src/refractor/TypeIntegrity.js +75 -0
  40. package/src/resolution/CircularDetector.js +91 -0
  41. package/src/resolution/ConfigLoader.js +87 -0
  42. package/src/resolution/DepencyResolver.js +133 -0
  43. package/src/resolution/DependencyProfiler.js +268 -0
  44. package/src/resolution/PathMapper.js +125 -0
  45. package/src/resolution/WorkSpaceGraph.js +474 -0
  46. package/tsconfig.json +26 -0
package/logo.png ADDED
Binary file
package/package.json ADDED
@@ -0,0 +1,96 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "name": "entkapp",
4
+ "version": "4.1.0",
5
+ "description": "The Ultimate Enterprise Codebase Janitor. Faster than Knip with OXC integration, type-aware analysis, and automated structural healing. Fully standalone - solving what Knip cannot.",
6
+ "type": "module",
7
+ "main": "index.js",
8
+ "bin": {
9
+ "entkapp": "./bin/cli.js"
10
+ },
11
+ "scripts": {
12
+ "start": "node bin/cli.js",
13
+ "entkapp:run": "npx entkapp --fix",
14
+ "test": "echo \"Error: no test specified\" && exit 0",
15
+ "test:stability": "npm run test",
16
+ "docs:dev": "vitepress dev docs",
17
+ "docs:build": "vitepress build docs && cp docs/sitemap.xml docs/.vitepress/dist/ && cp docs/robots.txt docs/.vitepress/dist && cp logo.png docs/.vitepress/dist",
18
+ "docs:preview": "vitepress preview docs"
19
+ },
20
+ "keywords": [
21
+ "analysis",
22
+ "analyze",
23
+ "ast",
24
+ "automation",
25
+ "cli",
26
+ "gui",
27
+ "class",
28
+ "dead-code",
29
+ "dependencies",
30
+ "dependency-analysis",
31
+ "dependency-audit",
32
+ "detect",
33
+ "devDependencies",
34
+ "duplicate",
35
+ "entropy",
36
+ "enum",
37
+ "export",
38
+ "files",
39
+ "find",
40
+ "ghost-dependencies",
41
+ "javascript",
42
+ "maintenance",
43
+ "members",
44
+ "missing",
45
+ "missing-imports",
46
+ "monorepo",
47
+ "namespace",
48
+ "oxc",
49
+ "package.json",
50
+ "packages",
51
+ "scan",
52
+ "security",
53
+ "secret-scanner",
54
+ "secrets",
55
+ "hardcoded-secrets",
56
+ "credentials",
57
+ "audit",
58
+ "structural-healing",
59
+ "setup",
60
+ "type",
61
+ "types",
62
+ "typescript",
63
+ "unreferenced",
64
+ "unresolved",
65
+ "unused",
66
+ "unused-packages",
67
+ "workspace"
68
+ ],
69
+ "author": "DreamLongYT",
70
+ "license": "Apache-2.0",
71
+ "repository": {
72
+ "type": "git",
73
+ "url": "git+https://github.com/DreamLongYT/entkapp.git"
74
+ },
75
+ "bugs": {
76
+ "url": "https://github.com/DreamLongYT/entkapp/issues"
77
+ },
78
+ "homepage": "https://dreamlongyt.github.io/entkapp/",
79
+ "dependencies": {
80
+ "ansis": "^3.0.0",
81
+ "commander": "^12.0.0",
82
+ "enhanced-resolve": "^5.16.0",
83
+ "execa": "^8.0.1",
84
+ "oxc-parser": "^0.135.0",
85
+ "typescript": "^5.0.0"
86
+ },
87
+ "devDependencies": {
88
+ "@types/node": "^25.9.3",
89
+ "knip": "^6.17.1",
90
+ "vitepress": "^1.6.4",
91
+ "vue": ">=3.5.0"
92
+ },
93
+ "engines": {
94
+ "node": ">=18.0.0"
95
+ }
96
+ }
@@ -0,0 +1,185 @@
1
+ import path from 'path';
2
+
3
+ /**
4
+ * Core In-Memory Graph Node Representation.
5
+ * Tracks structural metadata, dependencies, and usage metrics for a single file.
6
+ */
7
+ export class GraphNode {
8
+ constructor(filePath) {
9
+ this.filePath = filePath;
10
+ this.incomingEdges = new Set(); // Files that import THIS file
11
+ this.outgoingEdges = new Set(); // Files that THIS file imports
12
+
13
+ this.explicitImports = new Set(); // Raw import strings
14
+ this.importedSymbols = new Set(); // "file:symbol" or "file:*"
15
+ this.internalExports = new Map(); // symbol -> { type, start, end, ... }
16
+
17
+ this.externalPackageUsage = new Set();
18
+ this.instantiatedIdentifiers = new Set();
19
+ this.propertyAccessChains = new Set();
20
+ this.rawStringReferences = new Set();
21
+ this.jsxComponents = new Set();
22
+ this.jsxProps = new Set();
23
+ this.decorators = new Set();
24
+ this.dynamicImports = new Set();
25
+ this.calculatedDynamicImports = [];
26
+
27
+ this.isEntry = false;
28
+ this.isLibraryEntry = false;
29
+ this.isFrameworkComponent = false;
30
+ this.symbolSourceLocations = new Map(); // symbol -> { line, column }
31
+ this.localSuppressedRules = new Set();
32
+ }
33
+
34
+ isSymbolReferencedExternally(symbolName, projectGraph) {
35
+ if (this.isLibraryEntry) return true;
36
+
37
+ // --- NEW: Self-reference check (Fix for SecretSeverity bug) ---
38
+ // If the symbol is used within the same file, it is NOT dead.
39
+ if (this.instantiatedIdentifiers.has(symbolName)) return true;
40
+
41
+ // Check property access in the same file
42
+ for (const accessChain of this.propertyAccessChains) {
43
+ if (accessChain.endsWith(`.${symbolName}`) || accessChain.includes(`.${symbolName}.`)) {
44
+ return true;
45
+ }
46
+ }
47
+ // --------------------------------------------------------------
48
+
49
+ for (const parentPath of this.incomingEdges) {
50
+ const parentNode = projectGraph.get(parentPath);
51
+ if (!parentNode) continue;
52
+
53
+ // Strategy 1: Check absolute path based tokens
54
+ const absoluteImportKey = `${this.filePath}:${symbolName}`;
55
+ const absoluteStarKey = `${this.filePath}:*`;
56
+
57
+ if (parentNode.importedSymbols.has(absoluteImportKey) || parentNode.importedSymbols.has(absoluteStarKey)) {
58
+ return true;
59
+ }
60
+
61
+ // Strategy 2: Check relative path based tokens
62
+ const relativePath = path.relative(path.dirname(parentPath), this.filePath).replace(/\\/g, '/');
63
+ const relativePathNoExt = relativePath.replace(/\.(js|ts|tsx|jsx)$/, '');
64
+
65
+ const importKey = `${relativePath}:${symbolName}`;
66
+ const importKeyAlt = `${relativePathNoExt}:${symbolName}`;
67
+ const starKey = `${relativePath}:*`;
68
+ const starKeyAlt = `${relativePathNoExt}:*`;
69
+
70
+ if (parentNode.importedSymbols.has(importKey) || parentNode.importedSymbols.has(importKeyAlt) ||
71
+ parentNode.importedSymbols.has(starKey) || parentNode.importedSymbols.has(starKeyAlt)) {
72
+ return true;
73
+ }
74
+
75
+ if (parentNode.instantiatedIdentifiers.has(symbolName)) return true;
76
+
77
+ for (const accessChain of parentNode.propertyAccessChains) {
78
+ if (accessChain.endsWith(`.${symbolName}`) || accessChain.includes(`.${symbolName}.`)) {
79
+ return true;
80
+ }
81
+ }
82
+
83
+ if (parentNode.rawStringReferences.has(symbolName)) return true;
84
+ if (parentNode.jsxComponents.has(symbolName)) return true;
85
+
86
+ for (const jsxProp of parentNode.jsxProps) {
87
+ if (jsxProp.endsWith(`:${symbolName}`)) return true;
88
+ }
89
+
90
+ if (parentNode.decorators.has(symbolName)) return true;
91
+ }
92
+
93
+ return false;
94
+ }
95
+ }
96
+
97
+ export class EngineContext {
98
+ constructor(cwd) {
99
+ // FIX: Ensure cwd is always defined, fallback to process.cwd()
100
+ this.cwd = cwd || process.cwd();
101
+ this.projectGraph = new Map(); // Path -> GraphNode
102
+ this.usedExternalPackages = new Set();
103
+ this.unimportedUsedPackages = new Set(); // NEW: For "Unimported but used"
104
+ this.importedUnusedPackages = new Set(); // NEW: For "Imported but unused"
105
+ this.unusedBinaries = new Set(); // NEW: For "Unused Binaries"
106
+ this.manifestDependencies = new Map(); // NEW: Store dependencies from package.json
107
+
108
+ this.isWorkspaceEnabled = false;
109
+ this.monorepoPackageRoots = new Set();
110
+ this.verbose = false;
111
+ this.metrics = { totalFilesScanned: 0, cacheHits: 0, cacheMisses: 0 };
112
+ this.allSecretFindings = [];
113
+ }
114
+
115
+ getOrCreateNode(filePath) {
116
+ if (!this.projectGraph.has(filePath)) {
117
+ this.projectGraph.set(filePath, new GraphNode(filePath));
118
+ }
119
+ return this.projectGraph.get(filePath);
120
+ }
121
+
122
+ generateSummaryReport() {
123
+ const report = {
124
+ orphanedFiles: [],
125
+ deadExports: [],
126
+ unusedDependencies: [],
127
+ unimportedUsedPackages: [], // NEW
128
+ importedUnusedPackages: [], // NEW
129
+ unusedBinaries: [] // NEW
130
+ };
131
+
132
+ // 1. Files & Exports
133
+ for (const [filePath, node] of this.projectGraph.entries()) {
134
+ const isSuppressed = node.localSuppressedRules.has('*') || node.localSuppressedRules.has('unused-file');
135
+
136
+ if (node.incomingEdges.size === 0 && !node.isEntry && !node.isLibraryEntry && !node.isFrameworkComponent && !isSuppressed) {
137
+ report.orphanedFiles.push(path.relative(this.cwd, filePath));
138
+ }
139
+
140
+ for (const [symbol, meta] of node.internalExports.entries()) {
141
+ if (symbol === '*' || symbol === 'default' || node.localSuppressedRules.has('unused-export') || node.localSuppressedRules.has(`unused-export:${symbol}`)) {
142
+ continue;
143
+ }
144
+
145
+ if (!node.isSymbolReferencedExternally(symbol, this.projectGraph)) {
146
+ const loc = node.symbolSourceLocations.get(symbol) || { line: 0, column: 0 };
147
+ report.deadExports.push({
148
+ symbol,
149
+ file: path.relative(this.cwd, filePath),
150
+ line: loc.line
151
+ });
152
+ }
153
+ }
154
+ }
155
+
156
+ // 2. Dependency Analysis (Extended)
157
+ // Compare manifest dependencies with actually used packages
158
+ for (const [manifestPath, deps] of this.manifestDependencies.entries()) {
159
+ const allDeps = [...(deps.dependencies || []), ...(deps.devDependencies || [])];
160
+
161
+ for (const dep of allDeps) {
162
+ // Skip @types packages and known safe packages
163
+ if (dep.startsWith('@types/') || dep === 'entkapp') {
164
+ continue;
165
+ }
166
+
167
+ // Check if the dependency is actually used in the code
168
+ if (!this.usedExternalPackages.has(dep)) {
169
+ report.unusedDependencies.push({
170
+ package: dep,
171
+ type: deps.dependencies.includes(dep) ? 'dependency' : 'devDependency',
172
+ manifest: path.relative(this.cwd, manifestPath)
173
+ });
174
+ this.importedUnusedPackages.add(dep);
175
+ }
176
+ }
177
+ }
178
+
179
+ report.unimportedUsedPackages = Array.from(this.unimportedUsedPackages);
180
+ report.importedUnusedPackages = Array.from(this.importedUnusedPackages);
181
+ report.unusedBinaries = Array.from(this.unusedBinaries);
182
+
183
+ return report;
184
+ }
185
+ }
@@ -0,0 +1,376 @@
1
+ /**
2
+ * ============================================================================
3
+ * Headless API for entkapp v4.1.0
4
+ * ============================================================================
5
+ * Provides a programmatic interface for integrating entkapp into
6
+ * custom workflows, CI/CD pipelines, and third-party tools.
7
+ *
8
+ * Features:
9
+ * - Full control over analysis and refactoring operations
10
+ * - Event-driven architecture for real-time feedback
11
+ * - Streaming results for large codebases
12
+ * - Plugin SDK integration
13
+ */
14
+
15
+ import EventEmitter from 'events';
16
+ import path from 'path';
17
+ import { RefactoringEngine } from '../index.js';
18
+
19
+ export class HeadlessAPI extends EventEmitter {
20
+ constructor(options = {}) {
21
+ super();
22
+ this.options = options;
23
+ this.engine = null;
24
+ this.analysisResults = null;
25
+ this.isRunning = false;
26
+ }
27
+
28
+ /**
29
+ * Initialize the API with a project context
30
+ * @param {string} projectRoot - Root directory of the project
31
+ * @param {Object} config - Configuration options
32
+ * @returns {Promise<void>}
33
+ */
34
+ async initialize(projectRoot, config = {}) {
35
+ try {
36
+ this.emit('initialize:start', { projectRoot });
37
+
38
+ const engineOptions = {
39
+ cwd: projectRoot,
40
+ autoFix: config.autoFix !== false,
41
+ skipConfirm: config.skipConfirm || false,
42
+ verbose: config.verbose || false,
43
+ ...config
44
+ };
45
+
46
+ this.engine = new RefactoringEngine(engineOptions);
47
+
48
+ this.emit('initialize:complete', {
49
+ projectRoot,
50
+ config: this.engine.context
51
+ });
52
+ } catch (error) {
53
+ this.emit('initialize:error', { error });
54
+ throw error;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Analyze the codebase without making changes
60
+ * @returns {Promise<Object>} Analysis results
61
+ */
62
+ async analyze() {
63
+ if (!this.engine) {
64
+ throw new Error('API not initialized. Call initialize() first.');
65
+ }
66
+
67
+ try {
68
+ this.isRunning = true;
69
+ this.emit('analysis:start');
70
+
71
+ // Initialize path mappings and workspace graph
72
+ await this.engine.pathMapper.loadMappings(this.engine.context.tsconfigFilename);
73
+
74
+ if (this.engine.context.isWorkspaceEnabled) {
75
+ this.emit('analysis:workspace-mapping-start');
76
+ await this.engine.workspaceGraph.initializeWorkspaceMesh();
77
+ this.emit('analysis:workspace-mapping-complete');
78
+ }
79
+
80
+ // Load cache manifest
81
+ const cacheManifest = await this.engine.cacheManager.loadCacheManifest();
82
+
83
+ // Discover source files
84
+ this.emit('analysis:file-discovery-start');
85
+ const fileList = [];
86
+ await this.engine.discoverSourceFiles(this.engine.context.cwd, fileList);
87
+ this.engine.context.metrics.totalFilesScanned = fileList.length;
88
+ this.emit('analysis:file-discovery-complete', { fileCount: fileList.length });
89
+
90
+ // Identify framework ecosystems
91
+ this.emit('analysis:framework-detection-start');
92
+ const activeFrameworkEcosystems = await this.engine.magicDetector.identifyActiveProjectEcosystems(
93
+ this.engine.context.cwd
94
+ );
95
+ this.emit('analysis:framework-detection-complete', { ecosystems: activeFrameworkEcosystems });
96
+
97
+ // Process files
98
+ this.emit('analysis:file-processing-start');
99
+ const sourceCodeFilesList = [];
100
+ for (const file of fileList) {
101
+ if (file.endsWith('package.json')) {
102
+ await this.engine.auditManifestSupplyChain(file);
103
+ } else {
104
+ sourceCodeFilesList.push(file);
105
+ }
106
+ }
107
+
108
+ // Initialize TypeScript program for AST analysis (required before processFile)
109
+ if (sourceCodeFilesList.length > 0) {
110
+ try {
111
+ this.engine.analyzer.initProgram(sourceCodeFilesList);
112
+ } catch (e) {
113
+ if (this.engine.context.verbose) {
114
+ console.warn('Warning: Failed to initialize TypeScript program:', e.message);
115
+ }
116
+ }
117
+ }
118
+
119
+ // Parallel processing
120
+ let parallelParseCompleted = false;
121
+ if (sourceCodeFilesList.length > 10) {
122
+ parallelParseCompleted = await this.engine.workerPool.parallelAnalyzeCodebase(
123
+ sourceCodeFilesList,
124
+ this.engine
125
+ );
126
+ }
127
+
128
+ // Sequential processing
129
+ for (let i = 0; i < sourceCodeFilesList.length; i++) {
130
+ const filePath = sourceCodeFilesList[i];
131
+ const node = this.engine.context.getOrCreateNode(filePath);
132
+ const currentHash = await this.engine.cacheManager.computeHash(filePath);
133
+ node.contentHash = currentHash;
134
+
135
+ const isFileCached = cacheManifest[filePath] && cacheManifest[filePath].hash === currentHash;
136
+
137
+ if (isFileCached) {
138
+ this.engine.context.metrics.cacheHits++;
139
+ this.engine.hydrateNodeFromCache(node, cacheManifest[filePath]);
140
+ } else if (!parallelParseCompleted) {
141
+ this.engine.context.metrics.cacheMisses++;
142
+ await this.engine.analyzer.parseFile(filePath, "", node);
143
+ }
144
+
145
+ this.engine.magicDetector.injectVirtualConsumerEdges(filePath, node, activeFrameworkEcosystems);
146
+ node.externalPackageUsage.forEach(pkg => this.engine.context.usedExternalPackages.add(pkg));
147
+
148
+ // Emit progress
149
+ if ((i + 1) % Math.ceil(sourceCodeFilesList.length / 10) === 0) {
150
+ this.emit('analysis:file-processing-progress', {
151
+ processed: i + 1,
152
+ total: sourceCodeFilesList.length,
153
+ percentage: Math.round(((i + 1) / sourceCodeFilesList.length) * 100)
154
+ });
155
+ }
156
+ }
157
+
158
+ // Link dependency graph
159
+ this.emit('analysis:graph-linking-start');
160
+ await this.engine.linkDependencyGraph();
161
+ this.emit('analysis:graph-linking-complete');
162
+
163
+ // Generate summary
164
+ this.analysisResults = this.engine.context.generateSummaryReport();
165
+ this.emit('analysis:complete', this.analysisResults);
166
+
167
+ this.isRunning = false;
168
+ return this.analysisResults;
169
+ } catch (error) {
170
+ this.isRunning = false;
171
+ this.emit('analysis:error', { error });
172
+ throw error;
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Get detailed impact analysis for a specific file or export
178
+ * @param {string} filePath - Path to the file
179
+ * @param {string} symbol - Optional: specific export symbol to analyze
180
+ * @returns {Promise<Object>} Impact analysis results
181
+ */
182
+ async getImpactAnalysis(filePath, symbol = null) {
183
+ if (!this.engine) {
184
+ throw new Error('API not initialized. Call initialize() first.');
185
+ }
186
+
187
+ try {
188
+ const node = this.engine.context.projectGraph.get(filePath);
189
+ if (!node) {
190
+ throw new Error(`File not found in analysis graph: ${filePath}`);
191
+ }
192
+
193
+ const impact = {
194
+ file: filePath,
195
+ symbol,
196
+ directDependents: Array.from(node.incomingEdges),
197
+ dependencies: Array.from(node.outgoingEdges),
198
+ internalExports: symbol
199
+ ? [symbol]
200
+ : Array.from(node.internalExports.keys()),
201
+ externalPackages: Array.from(node.externalPackageUsage)
202
+ };
203
+
204
+ if (symbol) {
205
+ const safety = await this.engine.impactAnalyzer.verifyRefactorSafety(
206
+ filePath,
207
+ symbol,
208
+ this.engine.context.projectGraph
209
+ );
210
+ impact.refactorSafety = safety;
211
+ }
212
+
213
+ return impact;
214
+ } catch (error) {
215
+ this.emit('impact-analysis:error', { error, filePath, symbol });
216
+ throw error;
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Apply refactoring changes with automatic rollback on test failure
222
+ * @param {Object} changes - Changes to apply
223
+ * @returns {Promise<Object>} Refactoring results
224
+ */
225
+ async applyRefactoring(changes = {}) {
226
+ if (!this.engine) {
227
+ throw new Error('API not initialized. Call initialize() first.');
228
+ }
229
+
230
+ if (!this.analysisResults) {
231
+ throw new Error('No analysis results available. Call analyze() first.');
232
+ }
233
+
234
+ try {
235
+ this.isRunning = true;
236
+ this.emit('refactoring:start', changes);
237
+
238
+ const refactoringResults = {
239
+ filesDeleted: [],
240
+ exportsRemoved: [],
241
+ dependenciesRemoved: [],
242
+ errors: []
243
+ };
244
+
245
+ await this.engine.selfHealer.runSelfHealingLifecycle(async () => {
246
+ // Delete dead files
247
+ const filesToDelete = changes.deleteDeadFiles !== false
248
+ ? this.analysisResults.orphanedFiles
249
+ : [];
250
+
251
+ for (const relPath of filesToDelete) {
252
+ try {
253
+ const absPath = path.resolve(this.engine.context.cwd, relPath);
254
+ await this.engine.txManager.stageDeletion(absPath);
255
+ refactoringResults.filesDeleted.push(relPath);
256
+ this.emit('refactoring:file-deleted', { file: relPath });
257
+ } catch (error) {
258
+ refactoringResults.errors.push({ type: 'file-deletion', file: relPath, error: error.message });
259
+ this.emit('refactoring:error', { type: 'file-deletion', file: relPath, error });
260
+ }
261
+ }
262
+
263
+ // Remove unused exports
264
+ const exportsToRemove = changes.removeUnusedExports !== false
265
+ ? this.analysisResults.deadExports
266
+ : [];
267
+
268
+ for (const unusedExport of exportsToRemove) {
269
+ try {
270
+ const absPath = path.resolve(this.engine.context.cwd, unusedExport.file);
271
+ const node = this.engine.context.projectGraph.get(absPath);
272
+
273
+ if (node) {
274
+ const safety = await this.engine.impactAnalyzer.verifyRefactorSafety(
275
+ absPath,
276
+ unusedExport.symbol,
277
+ this.engine.context.projectGraph
278
+ );
279
+
280
+ if (safety.isSafeToPrune) {
281
+ const nextText = await this.engine.sourceRewriter.stripNamedExportSignature(
282
+ absPath,
283
+ unusedExport.symbol,
284
+ node.internalExports.get(unusedExport.symbol)
285
+ );
286
+
287
+ await this.engine.txManager.stageWrite(absPath, nextText);
288
+ await this.engine.typeIntegrity.synchronizeDeclarationFile(absPath, unusedExport.symbol);
289
+
290
+ refactoringResults.exportsRemoved.push(unusedExport);
291
+ this.emit('refactoring:export-removed', unusedExport);
292
+ }
293
+ }
294
+ } catch (error) {
295
+ refactoringResults.errors.push({
296
+ type: 'export-removal',
297
+ export: unusedExport.symbol,
298
+ file: unusedExport.file,
299
+ error: error.message
300
+ });
301
+ this.emit('refactoring:error', { type: 'export-removal', export: unusedExport, error });
302
+ }
303
+ }
304
+
305
+ // Remove unused dependencies
306
+ const depsToRemove = changes.removeUnusedDependencies !== false
307
+ ? this.analysisResults.unusedDependencies
308
+ : [];
309
+
310
+ for (const dep of depsToRemove) {
311
+ try {
312
+ const absPath = path.resolve(this.engine.context.cwd, dep.manifest);
313
+ // TODO: Implement package.json dependency removal
314
+ refactoringResults.dependenciesRemoved.push(dep);
315
+ this.emit('refactoring:dependency-removed', dep);
316
+ } catch (error) {
317
+ refactoringResults.errors.push({
318
+ type: 'dependency-removal',
319
+ package: dep.package,
320
+ error: error.message
321
+ });
322
+ this.emit('refactoring:error', { type: 'dependency-removal', package: dep.package, error });
323
+ }
324
+ }
325
+ });
326
+
327
+ this.isRunning = false;
328
+ this.emit('refactoring:complete', refactoringResults);
329
+ return refactoringResults;
330
+ } catch (error) {
331
+ this.isRunning = false;
332
+ this.emit('refactoring:error', { error });
333
+ throw error;
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Get current analysis metrics
339
+ * @returns {Object} Metrics
340
+ */
341
+ getMetrics() {
342
+ if (!this.engine) {
343
+ throw new Error('API not initialized. Call initialize() first.');
344
+ }
345
+ return this.engine.context.metrics;
346
+ }
347
+
348
+ /**
349
+ * Get all registered plugins
350
+ * @returns {Array} Plugin instances
351
+ */
352
+ getPlugins() {
353
+ if (!this.engine) {
354
+ throw new Error('API not initialized. Call initialize() first.');
355
+ }
356
+ return this.engine.context.pluginRegistry?.getPlugins() || [];
357
+ }
358
+
359
+ /**
360
+ * Get analysis results
361
+ * @returns {Object} Analysis results
362
+ */
363
+ getAnalysisResults() {
364
+ return this.analysisResults;
365
+ }
366
+
367
+ /**
368
+ * Check if API is currently running
369
+ * @returns {boolean}
370
+ */
371
+ isProcessing() {
372
+ return this.isRunning;
373
+ }
374
+ }
375
+
376
+ export default HeadlessAPI;