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
package/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # swynx-lite
2
+
3
+ Dead code detection and cleanup for 35 languages. One command, zero config.
4
+
5
+ ```bash
6
+ npx swynx-lite
7
+ ```
8
+
9
+ Like [Knip](https://knip.dev), but for every language. Plus security scanning. Plus it cleans up for you.
10
+
11
+ ## What it does
12
+
13
+ - **Detects dead code** across JS/TS, Python, Go, Java, Kotlin, Rust, C#, PHP, Ruby, Swift, and 25 more languages
14
+ - **Scans for security vulnerabilities** (CWE patterns) hiding in dead code
15
+ - **Removes dead code** with a quarantine safety net — undo anytime
16
+ - **Works in CI** with exit codes and configurable thresholds
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ npm install -g swynx-lite
22
+ ```
23
+
24
+ Or run directly:
25
+
26
+ ```bash
27
+ npx swynx-lite
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ ```bash
33
+ swynx-lite # Scan current directory
34
+ swynx-lite scan ./src # Scan a specific path
35
+ swynx-lite scan --json # Machine-readable output
36
+ swynx-lite scan --ci # CI mode (exit 1 if dead code found)
37
+ swynx-lite clean # Remove dead code (with quarantine)
38
+ swynx-lite clean --dry-run # Preview what would be removed
39
+ swynx-lite restore # Undo the last clean
40
+ swynx-lite purge # Permanently delete quarantined files
41
+ swynx-lite init # Create a config file
42
+ ```
43
+
44
+ ## Example output
45
+
46
+ ```
47
+ swynx lite v1.0.0
48
+
49
+ -- Summary -----------------------------------------------
50
+
51
+ Files scanned 1,247
52
+ Entry points 18
53
+ Reachable 1,198
54
+ Dead files 49 (3.93%)
55
+ Dead code size 284 KB
56
+
57
+ -- Dead Files ---------------------------------------------
58
+
59
+ src/utils/old-parser.ts 12.4 KB 318 lines
60
+ src/helpers/deprecated-auth.ts 8.1 KB 195 lines
61
+ src/lib/unused-validator.js 6.3 KB 142 lines
62
+ ... and 46 more
63
+
64
+ -- Security -----------------------------------------------
65
+
66
+ 2 findings in dead code
67
+
68
+ CRITICAL src/utils/old-parser.ts:42
69
+ CWE-94 Code Injection - eval() with dynamic input
70
+
71
+ -- What Next ----------------------------------------------
72
+
73
+ Run swynx-lite clean to remove 49 dead files (saves 284 KB)
74
+ ```
75
+
76
+ ## CI Integration
77
+
78
+ ```yaml
79
+ # GitHub Actions
80
+ - run: npx swynx-lite scan --ci --threshold 5
81
+ ```
82
+
83
+ Exit code 0 if dead code rate is below the threshold, 1 if above.
84
+
85
+ ## Config
86
+
87
+ Create a `.swynx-lite.json` in your project root (or run `swynx-lite init`):
88
+
89
+ ```json
90
+ {
91
+ "ignore": [
92
+ "**/__tests__/**",
93
+ "**/*.test.*",
94
+ "scripts/**"
95
+ ],
96
+ "ci": {
97
+ "threshold": 5,
98
+ "failOnSecurity": true
99
+ }
100
+ }
101
+ ```
102
+
103
+ You can also use a `.swynxignore` file (gitignore-style).
104
+
105
+ ## Swynx Pro
106
+
107
+ Swynx Lite is free forever. No telemetry, no tracking, fully offline.
108
+
109
+ For teams that need dashboards, predictive intelligence, dependency scanning, and enterprise reporting: [swynx.io/pro](https://swynx.io/pro)
110
+
111
+ ## License
112
+
113
+ BSL 1.1 (converts to Apache 2.0 after 4 years)
package/bin/swynx-lite ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ import('../src/cli.mjs');
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "swynx-lite",
3
+ "version": "1.0.0",
4
+ "description": "Dead code detection and cleanup for 35 languages",
5
+ "type": "module",
6
+ "bin": {
7
+ "swynx-lite": "./bin/swynx-lite"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "src/",
12
+ "LICENSE"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18.0.0"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/swynx-io/swynx-lite.git"
20
+ },
21
+ "homepage": "https://lite.swynx.io",
22
+ "scripts": {
23
+ "sync": "node scripts/sync-from-pro.mjs"
24
+ },
25
+ "keywords": [
26
+ "dead-code",
27
+ "deadcode",
28
+ "unused-code",
29
+ "unused-files",
30
+ "cleanup",
31
+ "security",
32
+ "cwe",
33
+ "static-analysis",
34
+ "quarantine",
35
+ "treeshaking",
36
+ "codebase-health",
37
+ "knip"
38
+ ],
39
+ "author": "Swynx",
40
+ "license": "BUSL-1.1",
41
+ "dependencies": {
42
+ "@babel/parser": "^7.24.0",
43
+ "@babel/traverse": "^7.24.0",
44
+ "commander": "^12.1.0",
45
+ "glob": "^10.3.0"
46
+ }
47
+ }
package/src/clean.mjs ADDED
@@ -0,0 +1,280 @@
1
+ // src/clean.mjs
2
+ // Clean orchestrator — scan, quarantine, delete, clean imports
3
+
4
+ import { resolve, join, relative, extname } from 'path';
5
+ import { existsSync, readFileSync, writeFileSync, appendFileSync } from 'fs';
6
+ import { glob } from 'glob';
7
+ import { loadConfig } from './config.mjs';
8
+ import { scanDeadCode } from './shared/scanner/scan-dead-code.mjs';
9
+ import { createSession, quarantineFile } from './shared/fixer/quarantine.mjs';
10
+ import { cleanDeadImports } from './shared/fixer/import-cleaner.mjs';
11
+ import { cleanBarrelExports } from './shared/fixer/barrel-cleaner.mjs';
12
+ import { ProgressSpinner } from './output/progress.mjs';
13
+ import { renderCleanOutput } from './output/console.mjs';
14
+ import { formatCleanJSON } from './output/json.mjs';
15
+
16
+ /**
17
+ * Run the clean command
18
+ */
19
+ export async function runClean(targetPath, cliOptions = {}) {
20
+ const projectPath = resolve(targetPath || '.');
21
+
22
+ if (!existsSync(projectPath)) {
23
+ console.error(` Error: path not found — ${projectPath}`);
24
+ return { exitCode: 2 };
25
+ }
26
+
27
+ const config = loadConfig(projectPath, cliOptions);
28
+ const isJSON = cliOptions.json || false;
29
+ const noColor = cliOptions.color === false || !!process.env.NO_COLOR;
30
+ const dryRun = cliOptions.dryRun || false;
31
+ const skipQuarantine = cliOptions.quarantine === false;
32
+ const skipImportClean = cliOptions.importClean === false || !config.clean.importClean;
33
+ const skipBarrelClean = cliOptions.barrelClean === false || !config.clean.barrelClean;
34
+ const autoYes = cliOptions.yes || false;
35
+
36
+ // Spinner
37
+ const spinner = new ProgressSpinner({
38
+ enabled: !isJSON,
39
+ noColor,
40
+ });
41
+
42
+ // Step 1: Scan
43
+ spinner.start('Scanning for dead code...');
44
+
45
+ let scanResults;
46
+ try {
47
+ scanResults = await scanDeadCode(projectPath, {
48
+ onProgress: (p) => spinner.update(p),
49
+ });
50
+ } catch (e) {
51
+ spinner.stop();
52
+ console.error(` Error during scan: ${e.message}`);
53
+ return { exitCode: 2 };
54
+ }
55
+
56
+ spinner.stop();
57
+
58
+ const deadFiles = scanResults.deadFiles || [];
59
+ if (deadFiles.length === 0) {
60
+ if (isJSON) {
61
+ console.log(formatCleanJSON({ dryRun, filesRemoved: 0, bytesRemoved: 0, importsRemoved: 0, barrelExportsRemoved: 0, files: [] }));
62
+ } else {
63
+ console.log('\n No dead code found. Your codebase is clean!\n');
64
+ }
65
+ return { exitCode: 0 };
66
+ }
67
+
68
+ const totalBytes = deadFiles.reduce((sum, f) => sum + (f.size || 0), 0);
69
+
70
+ // Step 2: Confirmation (unless --yes or --dry-run)
71
+ if (!dryRun && !autoYes && !isJSON) {
72
+ console.log('');
73
+ console.log(` ${deadFiles.length} dead file${deadFiles.length === 1 ? '' : 's'} found (${formatBytes(totalBytes)})`);
74
+ console.log('');
75
+
76
+ // Show top files
77
+ const showCount = Math.min(5, deadFiles.length);
78
+ for (let i = 0; i < showCount; i++) {
79
+ const f = deadFiles[i];
80
+ console.log(` ${(f.file || '').padEnd(42)} ${formatBytes(f.size).padStart(10)}`);
81
+ }
82
+ if (deadFiles.length > 5) {
83
+ console.log(` ... and ${deadFiles.length - 5} more`);
84
+ }
85
+ console.log('');
86
+
87
+ const ok = await confirm(` Remove ${deadFiles.length} file${deadFiles.length === 1 ? '' : 's'}? (y/N) `);
88
+ if (!ok) {
89
+ console.log(' Cancelled.\n');
90
+ return { exitCode: 0 };
91
+ }
92
+ }
93
+
94
+ // Step 3: Quarantine + delete
95
+ const deletedFiles = [];
96
+ let sessionId = null;
97
+ let bytesRemoved = 0;
98
+
99
+ if (!dryRun) {
100
+ if (!skipQuarantine) {
101
+ const session = createSession(projectPath, 'clean');
102
+ sessionId = session.sessionId;
103
+
104
+ spinner.start('Quarantining files...');
105
+ for (const f of deadFiles) {
106
+ const filePath = f.file || f.path || '';
107
+ const fullPath = join(projectPath, filePath);
108
+ try {
109
+ quarantineFile(projectPath, sessionId, fullPath);
110
+ deletedFiles.push(filePath);
111
+ bytesRemoved += f.size || 0;
112
+ } catch {
113
+ // Skip files that can't be quarantined
114
+ }
115
+ }
116
+ spinner.stop();
117
+ } else {
118
+ // Direct delete (no quarantine)
119
+ const { unlinkSync } = await import('fs');
120
+ spinner.start('Removing files...');
121
+ for (const f of deadFiles) {
122
+ const filePath = f.file || f.path || '';
123
+ const fullPath = join(projectPath, filePath);
124
+ try {
125
+ if (existsSync(fullPath)) {
126
+ unlinkSync(fullPath);
127
+ deletedFiles.push(filePath);
128
+ bytesRemoved += f.size || 0;
129
+ }
130
+ } catch { /* skip */ }
131
+ }
132
+ spinner.stop();
133
+ }
134
+
135
+ // Step 4: Clean dead imports from live files
136
+ let importsRemoved = 0;
137
+ let barrelExportsRemoved = 0;
138
+
139
+ if (deletedFiles.length > 0) {
140
+ // Find live JS/TS files
141
+ const livePatterns = join(projectPath, '**/*.{js,jsx,ts,tsx,mjs,cjs}');
142
+ const liveFiles = await glob(livePatterns, {
143
+ ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.swynx-quarantine/**'],
144
+ nodir: true,
145
+ });
146
+
147
+ const relativeLiveFiles = liveFiles
148
+ .map(f => relative(projectPath, f))
149
+ .filter(f => !deletedFiles.includes(f));
150
+
151
+ if (!skipImportClean) {
152
+ spinner.start('Cleaning dead imports...');
153
+ try {
154
+ const importResult = await cleanDeadImports(projectPath, deletedFiles, relativeLiveFiles);
155
+ importsRemoved = importResult.importsRemoved?.length || 0;
156
+ } catch { /* skip */ }
157
+ spinner.stop();
158
+ }
159
+
160
+ if (!skipBarrelClean) {
161
+ spinner.start('Cleaning barrel exports...');
162
+ try {
163
+ const barrelResult = await cleanBarrelExports(projectPath, deletedFiles, relativeLiveFiles);
164
+ barrelExportsRemoved = barrelResult.exportsRemoved?.length || 0;
165
+ } catch { /* skip */ }
166
+ spinner.stop();
167
+ }
168
+ }
169
+
170
+ // Step 5: Auto-add .swynx-quarantine/ to .gitignore
171
+ if (sessionId) {
172
+ ensureGitignore(projectPath);
173
+ }
174
+
175
+ // Output
176
+ const result = {
177
+ dryRun: false,
178
+ filesRemoved: deletedFiles.length,
179
+ bytesRemoved,
180
+ importsRemoved,
181
+ barrelExportsRemoved,
182
+ sessionId,
183
+ deadCount: deadFiles.length,
184
+ files: deadFiles.map(f => ({ file: f.file, size: f.size })),
185
+ };
186
+
187
+ if (isJSON) {
188
+ console.log(formatCleanJSON(result));
189
+ } else {
190
+ console.log(renderCleanOutput(result, { noColor }));
191
+ }
192
+
193
+ return { exitCode: 0, result };
194
+ }
195
+
196
+ // Dry run output
197
+ let importsWouldRemove = 0;
198
+ let barrelsWouldRemove = 0;
199
+
200
+ // Estimate import/barrel cleanups
201
+ const deletedRelPaths = deadFiles.map(f => f.file || f.path || '');
202
+ const livePatterns = join(projectPath, '**/*.{js,jsx,ts,tsx,mjs,cjs}');
203
+ const liveFiles = await glob(livePatterns, {
204
+ ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.swynx-quarantine/**'],
205
+ nodir: true,
206
+ });
207
+ const relativeLiveFiles = liveFiles
208
+ .map(f => relative(projectPath, f))
209
+ .filter(f => !deletedRelPaths.includes(f));
210
+
211
+ if (!skipImportClean) {
212
+ try {
213
+ const importResult = await cleanDeadImports(projectPath, deletedRelPaths, relativeLiveFiles, { dryRun: true });
214
+ importsWouldRemove = importResult.importsRemoved?.length || 0;
215
+ } catch { /* skip */ }
216
+ }
217
+
218
+ if (!skipBarrelClean) {
219
+ try {
220
+ const barrelResult = await cleanBarrelExports(projectPath, deletedRelPaths, relativeLiveFiles, { dryRun: true });
221
+ barrelsWouldRemove = barrelResult.exportsRemoved?.length || 0;
222
+ } catch { /* skip */ }
223
+ }
224
+
225
+ const dryResult = {
226
+ dryRun: true,
227
+ filesRemoved: deadFiles.length,
228
+ bytesRemoved: totalBytes,
229
+ importsRemoved: importsWouldRemove,
230
+ barrelExportsRemoved: barrelsWouldRemove,
231
+ files: deadFiles.map(f => ({ file: f.file, size: f.size })),
232
+ };
233
+
234
+ if (isJSON) {
235
+ console.log(formatCleanJSON(dryResult));
236
+ } else {
237
+ console.log(renderCleanOutput(dryResult, { noColor, dryRun: true }));
238
+ }
239
+
240
+ return { exitCode: 0 };
241
+ }
242
+
243
+ /**
244
+ * Add .swynx-quarantine/ to .gitignore if not already there
245
+ */
246
+ function ensureGitignore(projectPath) {
247
+ const gitignorePath = join(projectPath, '.gitignore');
248
+ const entry = '.swynx-quarantine/';
249
+
250
+ try {
251
+ if (existsSync(gitignorePath)) {
252
+ const content = readFileSync(gitignorePath, 'utf-8');
253
+ if (content.includes(entry)) return;
254
+ appendFileSync(gitignorePath, `\n# Swynx Lite quarantine\n${entry}\n`);
255
+ } else {
256
+ writeFileSync(gitignorePath, `# Swynx Lite quarantine\n${entry}\n`);
257
+ }
258
+ } catch { /* best effort */ }
259
+ }
260
+
261
+ function formatBytes(bytes) {
262
+ if (bytes < 1024) return `${bytes} B`;
263
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
264
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
265
+ }
266
+
267
+ /**
268
+ * Simple stdin confirmation prompt
269
+ */
270
+ function confirm(message) {
271
+ return new Promise((resolve) => {
272
+ process.stdout.write(message);
273
+ process.stdin.setEncoding('utf-8');
274
+ process.stdin.resume();
275
+ process.stdin.once('data', (data) => {
276
+ process.stdin.pause();
277
+ resolve(data.trim().toLowerCase() === 'y');
278
+ });
279
+ });
280
+ }