swynx-lite 1.0.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 (36) hide show
  1. package/README.md +113 -0
  2. package/bin/swynx-lite +3 -0
  3. package/package.json +47 -0
  4. package/src/clean.mjs +280 -0
  5. package/src/cli.mjs +264 -0
  6. package/src/config.mjs +121 -0
  7. package/src/output/console.mjs +298 -0
  8. package/src/output/json.mjs +76 -0
  9. package/src/output/progress.mjs +57 -0
  10. package/src/scan.mjs +143 -0
  11. package/src/security.mjs +62 -0
  12. package/src/shared/fixer/barrel-cleaner.mjs +192 -0
  13. package/src/shared/fixer/import-cleaner.mjs +237 -0
  14. package/src/shared/fixer/quarantine.mjs +218 -0
  15. package/src/shared/scanner/analysers/buildSystems.mjs +647 -0
  16. package/src/shared/scanner/analysers/configParsers.mjs +1086 -0
  17. package/src/shared/scanner/analysers/deadcode.mjs +6194 -0
  18. package/src/shared/scanner/analysers/entryPointDetector.mjs +634 -0
  19. package/src/shared/scanner/analysers/generatedCode.mjs +297 -0
  20. package/src/shared/scanner/analysers/imports.mjs +60 -0
  21. package/src/shared/scanner/discovery.mjs +240 -0
  22. package/src/shared/scanner/parse-worker.mjs +82 -0
  23. package/src/shared/scanner/parsers/assets.mjs +44 -0
  24. package/src/shared/scanner/parsers/csharp.mjs +400 -0
  25. package/src/shared/scanner/parsers/css.mjs +60 -0
  26. package/src/shared/scanner/parsers/go.mjs +445 -0
  27. package/src/shared/scanner/parsers/java.mjs +364 -0
  28. package/src/shared/scanner/parsers/javascript.mjs +823 -0
  29. package/src/shared/scanner/parsers/kotlin.mjs +350 -0
  30. package/src/shared/scanner/parsers/python.mjs +497 -0
  31. package/src/shared/scanner/parsers/registry.mjs +233 -0
  32. package/src/shared/scanner/parsers/rust.mjs +427 -0
  33. package/src/shared/scanner/scan-dead-code.mjs +316 -0
  34. package/src/shared/security/patterns.mjs +349 -0
  35. package/src/shared/security/proximity.mjs +84 -0
  36. package/src/shared/security/scanner.mjs +269 -0
@@ -0,0 +1,237 @@
1
+ // src/fixer/import-cleaner.mjs
2
+ // Clean up import statements in live files that reference deleted files
3
+
4
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
5
+ import { join, dirname, relative, basename, extname } from 'path';
6
+
7
+ /**
8
+ * Find and clean imports that reference deleted files
9
+ * @param {string} projectPath - Project root
10
+ * @param {string[]} deletedFiles - List of deleted file paths (relative)
11
+ * @param {string[]} liveFiles - List of live files to check (relative)
12
+ * @param {object} options - Options
13
+ */
14
+ export async function cleanDeadImports(projectPath, deletedFiles, liveFiles, options = {}) {
15
+ const { dryRun = false } = options;
16
+
17
+ const result = {
18
+ filesModified: [],
19
+ importsRemoved: [],
20
+ errors: []
21
+ };
22
+
23
+ // Build a set of deleted file patterns (without extensions for matching)
24
+ const deletedPatterns = new Set();
25
+ for (const file of deletedFiles) {
26
+ // Add full path
27
+ deletedPatterns.add(file);
28
+ // Add without extension
29
+ const ext = extname(file);
30
+ if (ext) {
31
+ deletedPatterns.add(file.slice(0, -ext.length));
32
+ }
33
+ // Add just the basename without extension
34
+ const base = basename(file, ext);
35
+ deletedPatterns.add(base);
36
+ }
37
+
38
+ // Process each live file
39
+ for (const liveFile of liveFiles) {
40
+ const fullPath = join(projectPath, liveFile);
41
+
42
+ if (!existsSync(fullPath)) continue;
43
+
44
+ // Only process JS/TS files
45
+ const ext = extname(liveFile).toLowerCase();
46
+ if (!['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.vue'].includes(ext)) {
47
+ continue;
48
+ }
49
+
50
+ try {
51
+ const content = readFileSync(fullPath, 'utf-8');
52
+ const { modified, changes } = cleanImportsInFile(content, liveFile, deletedFiles, deletedPatterns);
53
+
54
+ if (changes.length > 0) {
55
+ if (!dryRun) {
56
+ writeFileSync(fullPath, modified, 'utf-8');
57
+ }
58
+
59
+ result.filesModified.push(liveFile);
60
+ result.importsRemoved.push({
61
+ file: liveFile,
62
+ imports: changes,
63
+ dryRun
64
+ });
65
+ }
66
+ } catch (error) {
67
+ result.errors.push({ file: liveFile, error: error.message });
68
+ }
69
+ }
70
+
71
+ return result;
72
+ }
73
+
74
+ /**
75
+ * Clean imports in a single file's content
76
+ */
77
+ function cleanImportsInFile(content, filePath, deletedFiles, deletedPatterns) {
78
+ const lines = content.split('\n');
79
+ const changes = [];
80
+ const newLines = [];
81
+ const fileDir = dirname(filePath);
82
+
83
+ for (let i = 0; i < lines.length; i++) {
84
+ const line = lines[i];
85
+ const importMatch = matchImportStatement(line);
86
+
87
+ if (importMatch) {
88
+ const { importPath, fullMatch } = importMatch;
89
+
90
+ // Resolve the import path relative to the file
91
+ const resolvedPath = resolveImportPath(fileDir, importPath, deletedFiles);
92
+
93
+ if (resolvedPath && isDeletedImport(resolvedPath, deletedFiles, deletedPatterns)) {
94
+ changes.push({
95
+ line: i + 1,
96
+ removed: line.trim(),
97
+ importPath
98
+ });
99
+ // Skip this line (don't add to newLines)
100
+ // Also skip empty lines that follow import blocks
101
+ continue;
102
+ }
103
+ }
104
+
105
+ newLines.push(line);
106
+ }
107
+
108
+ // Clean up consecutive empty lines that might be left
109
+ const cleaned = cleanEmptyLines(newLines);
110
+
111
+ return {
112
+ modified: cleaned.join('\n'),
113
+ changes
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Match import/require statements
119
+ */
120
+ function matchImportStatement(line) {
121
+ const trimmed = line.trim();
122
+
123
+ // ES6 import
124
+ // import X from 'path'
125
+ // import { X } from 'path'
126
+ // import * as X from 'path'
127
+ // import 'path'
128
+ const esImportMatch = trimmed.match(/^import\s+(?:.*\s+from\s+)?['"]([^'"]+)['"]/);
129
+ if (esImportMatch) {
130
+ return { importPath: esImportMatch[1], fullMatch: esImportMatch[0] };
131
+ }
132
+
133
+ // Dynamic import (inline - harder to remove safely, skip for now)
134
+ // const X = await import('path')
135
+
136
+ // CommonJS require
137
+ // const X = require('path')
138
+ // require('path')
139
+ const requireMatch = trimmed.match(/(?:const|let|var)?\s*\w*\s*=?\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/);
140
+ if (requireMatch) {
141
+ return { importPath: requireMatch[1], fullMatch: requireMatch[0] };
142
+ }
143
+
144
+ return null;
145
+ }
146
+
147
+ /**
148
+ * Resolve an import path relative to the importing file
149
+ */
150
+ function resolveImportPath(fileDir, importPath, deletedFiles) {
151
+ // Skip node_modules and absolute imports
152
+ if (!importPath.startsWith('.') && !importPath.startsWith('/')) {
153
+ return null;
154
+ }
155
+
156
+ // Resolve relative path
157
+ const resolved = join(fileDir, importPath);
158
+
159
+ // Try with common extensions
160
+ const extensions = ['', '.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.vue', '/index.js', '/index.ts'];
161
+
162
+ for (const ext of extensions) {
163
+ const withExt = resolved + ext;
164
+ // Normalize the path
165
+ const normalized = withExt.replace(/\\/g, '/').replace(/^\.\//, '');
166
+
167
+ for (const deleted of deletedFiles) {
168
+ const deletedNorm = deleted.replace(/\\/g, '/').replace(/^\.\//, '');
169
+ if (normalized === deletedNorm) {
170
+ return normalized;
171
+ }
172
+ // Check without extension
173
+ const deletedNoExt = deletedNorm.replace(/\.[^/.]+$/, '');
174
+ const resolvedNoExt = normalized.replace(/\.[^/.]+$/, '');
175
+ if (resolvedNoExt === deletedNoExt) {
176
+ return normalized;
177
+ }
178
+ }
179
+ }
180
+
181
+ return resolved;
182
+ }
183
+
184
+ /**
185
+ * Check if an import path matches a deleted file
186
+ */
187
+ function isDeletedImport(resolvedPath, deletedFiles, deletedPatterns) {
188
+ const normalized = resolvedPath.replace(/\\/g, '/').replace(/^\.\//, '');
189
+
190
+ // Direct match
191
+ if (deletedPatterns.has(normalized)) {
192
+ return true;
193
+ }
194
+
195
+ // Without extension match
196
+ const withoutExt = normalized.replace(/\.[^/.]+$/, '');
197
+ if (deletedPatterns.has(withoutExt)) {
198
+ return true;
199
+ }
200
+
201
+ // Check against full deleted paths
202
+ for (const deleted of deletedFiles) {
203
+ const deletedNorm = deleted.replace(/\\/g, '/').replace(/^\.\//, '');
204
+ const deletedNoExt = deletedNorm.replace(/\.[^/.]+$/, '');
205
+
206
+ if (normalized === deletedNorm || withoutExt === deletedNoExt) {
207
+ return true;
208
+ }
209
+ }
210
+
211
+ return false;
212
+ }
213
+
214
+ /**
215
+ * Clean up consecutive empty lines
216
+ */
217
+ function cleanEmptyLines(lines) {
218
+ const result = [];
219
+ let lastWasEmpty = false;
220
+
221
+ for (const line of lines) {
222
+ const isEmpty = line.trim() === '';
223
+
224
+ if (isEmpty && lastWasEmpty) {
225
+ continue; // Skip consecutive empty lines
226
+ }
227
+
228
+ result.push(line);
229
+ lastWasEmpty = isEmpty;
230
+ }
231
+
232
+ return result;
233
+ }
234
+
235
+ export default {
236
+ cleanDeadImports
237
+ };
@@ -0,0 +1,218 @@
1
+ // src/fixer/quarantine.mjs
2
+ // Quarantine manager for safe file removal
3
+
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync, unlinkSync, rmSync, readdirSync, statSync } from 'fs';
5
+ import { join, dirname, relative } from 'path';
6
+ import { randomUUID } from 'crypto';
7
+
8
+ const QUARANTINE_DIR = '.swynx-quarantine';
9
+
10
+ /**
11
+ * Create a new quarantine session
12
+ */
13
+ export function createSession(projectPath, reason = 'manual') {
14
+ const sessionId = `${Date.now()}-${randomUUID().slice(0, 8)}`;
15
+ const sessionDir = join(projectPath, QUARANTINE_DIR, sessionId);
16
+
17
+ mkdirSync(sessionDir, { recursive: true });
18
+
19
+ const manifest = {
20
+ sessionId,
21
+ reason,
22
+ createdAt: new Date().toISOString(),
23
+ projectPath,
24
+ files: [],
25
+ status: 'active',
26
+ fileCount: 0,
27
+ totalSize: 0
28
+ };
29
+
30
+ writeFileSync(join(sessionDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
31
+
32
+ return { sessionId, sessionDir, manifest };
33
+ }
34
+
35
+ /**
36
+ * Quarantine a file (move to quarantine directory)
37
+ */
38
+ export function quarantineFile(projectPath, sessionId, filePath) {
39
+ const sessionDir = join(projectPath, QUARANTINE_DIR, sessionId);
40
+ const manifestPath = join(sessionDir, 'manifest.json');
41
+
42
+ if (!existsSync(manifestPath)) {
43
+ throw new Error(`Quarantine session ${sessionId} not found`);
44
+ }
45
+
46
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
47
+
48
+ // Get relative path from project root
49
+ const relativePath = relative(projectPath, filePath);
50
+ const quarantinePath = join(sessionDir, 'files', relativePath);
51
+
52
+ // Ensure directory exists
53
+ mkdirSync(dirname(quarantinePath), { recursive: true });
54
+
55
+ // Get file size before moving
56
+ let fileSize = 0;
57
+ if (existsSync(filePath)) {
58
+ fileSize = statSync(filePath).size;
59
+ // Copy file to quarantine
60
+ copyFileSync(filePath, quarantinePath);
61
+ // Delete original
62
+ unlinkSync(filePath);
63
+ }
64
+
65
+ // Update manifest
66
+ manifest.files.push({
67
+ originalPath: relativePath,
68
+ quarantinePath: relative(sessionDir, quarantinePath),
69
+ quarantinedAt: new Date().toISOString(),
70
+ size: fileSize
71
+ });
72
+ manifest.fileCount = manifest.files.length;
73
+ manifest.totalSize = manifest.files.reduce((sum, f) => sum + (f.size || 0), 0);
74
+
75
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
76
+
77
+ return { quarantinePath, relativePath };
78
+ }
79
+
80
+ /**
81
+ * List all quarantine sessions for a project
82
+ */
83
+ export function listSessions(projectPath) {
84
+ const quarantineDir = join(projectPath, QUARANTINE_DIR);
85
+
86
+ if (!existsSync(quarantineDir)) {
87
+ return [];
88
+ }
89
+
90
+ const sessions = [];
91
+ const entries = readdirSync(quarantineDir, { withFileTypes: true });
92
+
93
+ for (const entry of entries) {
94
+ if (entry.isDirectory()) {
95
+ const manifestPath = join(quarantineDir, entry.name, 'manifest.json');
96
+ if (existsSync(manifestPath)) {
97
+ try {
98
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
99
+ sessions.push(manifest);
100
+ } catch (e) {
101
+ // Skip invalid sessions
102
+ }
103
+ }
104
+ }
105
+ }
106
+
107
+ // Sort by creation date (newest first)
108
+ return sessions.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
109
+ }
110
+
111
+ /**
112
+ * Get a specific session
113
+ */
114
+ export function getSession(projectPath, sessionId) {
115
+ const manifestPath = join(projectPath, QUARANTINE_DIR, sessionId, 'manifest.json');
116
+
117
+ if (!existsSync(manifestPath)) {
118
+ return null;
119
+ }
120
+
121
+ return JSON.parse(readFileSync(manifestPath, 'utf-8'));
122
+ }
123
+
124
+ /**
125
+ * Restore files from quarantine (undo)
126
+ */
127
+ export function restoreSession(projectPath, sessionId) {
128
+ const sessionDir = join(projectPath, QUARANTINE_DIR, sessionId);
129
+ const manifestPath = join(sessionDir, 'manifest.json');
130
+
131
+ if (!existsSync(manifestPath)) {
132
+ throw new Error(`Quarantine session ${sessionId} not found`);
133
+ }
134
+
135
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
136
+ const restored = [];
137
+ const errors = [];
138
+
139
+ for (const file of manifest.files) {
140
+ try {
141
+ const quarantinePath = join(sessionDir, file.quarantinePath || join('files', file.originalPath));
142
+ const originalPath = join(projectPath, file.originalPath);
143
+
144
+ if (existsSync(quarantinePath)) {
145
+ // Ensure directory exists
146
+ mkdirSync(dirname(originalPath), { recursive: true });
147
+ // Copy back
148
+ copyFileSync(quarantinePath, originalPath);
149
+ restored.push(file.originalPath);
150
+ } else {
151
+ errors.push({ file: file.originalPath, error: 'Quarantine file not found' });
152
+ }
153
+ } catch (error) {
154
+ errors.push({ file: file.originalPath, error: error.message });
155
+ }
156
+ }
157
+
158
+ // Update manifest status
159
+ manifest.status = 'restored';
160
+ manifest.restoredAt = new Date().toISOString();
161
+ manifest.restoredFiles = restored.length;
162
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
163
+
164
+ return {
165
+ success: true,
166
+ sessionId,
167
+ restored,
168
+ errors: errors.length > 0 ? errors : undefined,
169
+ message: `Restored ${restored.length} file(s)`
170
+ };
171
+ }
172
+
173
+ /**
174
+ * Permanently delete quarantined files
175
+ */
176
+ export function purgeSession(projectPath, sessionId) {
177
+ const sessionDir = join(projectPath, QUARANTINE_DIR, sessionId);
178
+
179
+ if (!existsSync(sessionDir)) {
180
+ throw new Error(`Quarantine session ${sessionId} not found`);
181
+ }
182
+
183
+ // Read manifest for logging
184
+ const manifestPath = join(sessionDir, 'manifest.json');
185
+ let fileCount = 0;
186
+ if (existsSync(manifestPath)) {
187
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
188
+ fileCount = manifest.fileCount || manifest.files?.length || 0;
189
+ }
190
+
191
+ // Remove entire session directory
192
+ rmSync(sessionDir, { recursive: true, force: true });
193
+
194
+ return {
195
+ success: true,
196
+ sessionId,
197
+ purgedFiles: fileCount,
198
+ message: `Permanently deleted quarantine session with ${fileCount} file(s)`
199
+ };
200
+ }
201
+
202
+ /**
203
+ * Get total quarantine size for a project
204
+ */
205
+ export function getQuarantineSize(projectPath) {
206
+ const sessions = listSessions(projectPath);
207
+ return sessions.reduce((sum, s) => sum + (s.totalSize || 0), 0);
208
+ }
209
+
210
+ export default {
211
+ createSession,
212
+ quarantineFile,
213
+ listSessions,
214
+ getSession,
215
+ restoreSession,
216
+ purgeSession,
217
+ getQuarantineSize
218
+ };