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.
- package/README.md +113 -0
- package/bin/swynx-lite +3 -0
- package/package.json +47 -0
- package/src/clean.mjs +280 -0
- package/src/cli.mjs +264 -0
- package/src/config.mjs +121 -0
- package/src/output/console.mjs +298 -0
- package/src/output/json.mjs +76 -0
- package/src/output/progress.mjs +57 -0
- package/src/scan.mjs +143 -0
- package/src/security.mjs +62 -0
- package/src/shared/fixer/barrel-cleaner.mjs +192 -0
- package/src/shared/fixer/import-cleaner.mjs +237 -0
- package/src/shared/fixer/quarantine.mjs +218 -0
- package/src/shared/scanner/analysers/buildSystems.mjs +647 -0
- package/src/shared/scanner/analysers/configParsers.mjs +1086 -0
- package/src/shared/scanner/analysers/deadcode.mjs +6194 -0
- package/src/shared/scanner/analysers/entryPointDetector.mjs +634 -0
- package/src/shared/scanner/analysers/generatedCode.mjs +297 -0
- package/src/shared/scanner/analysers/imports.mjs +60 -0
- package/src/shared/scanner/discovery.mjs +240 -0
- package/src/shared/scanner/parse-worker.mjs +82 -0
- package/src/shared/scanner/parsers/assets.mjs +44 -0
- package/src/shared/scanner/parsers/csharp.mjs +400 -0
- package/src/shared/scanner/parsers/css.mjs +60 -0
- package/src/shared/scanner/parsers/go.mjs +445 -0
- package/src/shared/scanner/parsers/java.mjs +364 -0
- package/src/shared/scanner/parsers/javascript.mjs +823 -0
- package/src/shared/scanner/parsers/kotlin.mjs +350 -0
- package/src/shared/scanner/parsers/python.mjs +497 -0
- package/src/shared/scanner/parsers/registry.mjs +233 -0
- package/src/shared/scanner/parsers/rust.mjs +427 -0
- package/src/shared/scanner/scan-dead-code.mjs +316 -0
- package/src/shared/security/patterns.mjs +349 -0
- package/src/shared/security/proximity.mjs +84 -0
- package/src/shared/security/scanner.mjs +269 -0
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// src/cli.mjs
|
|
2
|
+
// Commander-based CLI for swynx-lite
|
|
3
|
+
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
6
|
+
import { join, resolve } from 'path';
|
|
7
|
+
|
|
8
|
+
const VERSION = '1.0.0';
|
|
9
|
+
|
|
10
|
+
// ── First-run detection ──────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
function checkFirstRun() {
|
|
13
|
+
const marker = join(
|
|
14
|
+
process.env.HOME || process.env.USERPROFILE || '/tmp',
|
|
15
|
+
'.swynx-lite-init'
|
|
16
|
+
);
|
|
17
|
+
if (!existsSync(marker)) {
|
|
18
|
+
try { writeFileSync(marker, Date.now().toString()); } catch { /* */ }
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ── CLI Setup ────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
const program = new Command();
|
|
27
|
+
|
|
28
|
+
program
|
|
29
|
+
.name('swynx-lite')
|
|
30
|
+
.description('Dead code detection and cleanup for 35 languages')
|
|
31
|
+
.version(VERSION, '-v, --version');
|
|
32
|
+
|
|
33
|
+
// ── scan ─────────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
program
|
|
36
|
+
.command('scan')
|
|
37
|
+
.argument('[path]', 'Path to scan', '.')
|
|
38
|
+
.description('Scan for dead code and security issues')
|
|
39
|
+
.option('--json', 'Output as JSON instead of console')
|
|
40
|
+
.option('--ci', 'CI mode: exit code 1 if dead code found')
|
|
41
|
+
.option('--threshold <n>', 'In CI mode, only fail if dead rate > n%', parseFloat)
|
|
42
|
+
.option('--no-security', 'Skip security scanning')
|
|
43
|
+
.option('--ignore <glob>', 'Additional paths to ignore (repeatable)', collect, [])
|
|
44
|
+
.option('--verbose', 'Show detailed progress')
|
|
45
|
+
.option('--no-color', 'Disable ANSI colours')
|
|
46
|
+
.action(async (path, opts) => {
|
|
47
|
+
if (checkFirstRun() && !opts.json && !opts.ci) {
|
|
48
|
+
console.log(`\n ${dim('swynx lite — no telemetry, no tracking, fully offline.')}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const { runScan } = await import('./scan.mjs');
|
|
52
|
+
const { exitCode } = await runScan(path, opts);
|
|
53
|
+
process.exit(exitCode);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// ── clean ────────────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
program
|
|
59
|
+
.command('clean')
|
|
60
|
+
.argument('[path]', 'Path to clean', '.')
|
|
61
|
+
.description('Remove dead code with quarantine safety net')
|
|
62
|
+
.option('--dry-run', 'Preview what would be removed')
|
|
63
|
+
.option('--no-quarantine', 'Delete directly without quarantine backup')
|
|
64
|
+
.option('--no-import-clean', 'Skip cleaning dead imports from live files')
|
|
65
|
+
.option('--no-barrel-clean', 'Skip cleaning dead re-exports from barrel files')
|
|
66
|
+
.option('--yes', 'Skip confirmation prompt')
|
|
67
|
+
.option('--json', 'Output as JSON')
|
|
68
|
+
.option('--no-color', 'Disable ANSI colours')
|
|
69
|
+
.action(async (path, opts) => {
|
|
70
|
+
const { runClean } = await import('./clean.mjs');
|
|
71
|
+
const { exitCode } = await runClean(path, opts);
|
|
72
|
+
process.exit(exitCode);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ── restore ──────────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
program
|
|
78
|
+
.command('restore')
|
|
79
|
+
.description('Undo the last clean operation')
|
|
80
|
+
.option('--list', 'List all available restore points')
|
|
81
|
+
.option('--id <id>', 'Restore a specific quarantine session')
|
|
82
|
+
.option('--no-color', 'Disable ANSI colours')
|
|
83
|
+
.action(async (opts) => {
|
|
84
|
+
const { listSessions, restoreSession } = await import('./shared/fixer/quarantine.mjs');
|
|
85
|
+
const { renderRestoreOutput, renderSessionList } = await import('./output/console.mjs');
|
|
86
|
+
const noColor = opts.color === false || !!process.env.NO_COLOR;
|
|
87
|
+
const projectPath = resolve('.');
|
|
88
|
+
|
|
89
|
+
if (opts.list) {
|
|
90
|
+
const sessions = listSessions(projectPath);
|
|
91
|
+
console.log(renderSessionList(sessions, { noColor }));
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const sessions = listSessions(projectPath);
|
|
96
|
+
if (sessions.length === 0) {
|
|
97
|
+
console.log('\n No quarantine sessions found.\n');
|
|
98
|
+
process.exit(0);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Restore specific or most recent active session
|
|
102
|
+
const sessionId = opts.id || sessions.find(s => s.status === 'active')?.sessionId || sessions[0].sessionId;
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const result = restoreSession(projectPath, sessionId);
|
|
106
|
+
console.log(renderRestoreOutput(result, { noColor }));
|
|
107
|
+
} catch (e) {
|
|
108
|
+
console.error(` Error: ${e.message}\n`);
|
|
109
|
+
process.exit(2);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// ── purge ────────────────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
program
|
|
116
|
+
.command('purge')
|
|
117
|
+
.description('Permanently delete quarantined files')
|
|
118
|
+
.option('--id <id>', 'Purge a specific session only')
|
|
119
|
+
.option('--yes', 'Skip confirmation')
|
|
120
|
+
.option('--no-color', 'Disable ANSI colours')
|
|
121
|
+
.action(async (opts) => {
|
|
122
|
+
const { listSessions, purgeSession } = await import('./shared/fixer/quarantine.mjs');
|
|
123
|
+
const { renderPurgeOutput } = await import('./output/console.mjs');
|
|
124
|
+
const noColor = opts.color === false || !!process.env.NO_COLOR;
|
|
125
|
+
const projectPath = resolve('.');
|
|
126
|
+
|
|
127
|
+
const sessions = listSessions(projectPath);
|
|
128
|
+
if (sessions.length === 0) {
|
|
129
|
+
console.log('\n No quarantine sessions to purge.\n');
|
|
130
|
+
process.exit(0);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const autoYes = opts.yes || false;
|
|
134
|
+
|
|
135
|
+
if (!autoYes) {
|
|
136
|
+
const totalSize = sessions.reduce((sum, s) => sum + (s.totalSize || 0), 0);
|
|
137
|
+
const totalFiles = sessions.reduce((sum, s) => sum + (s.fileCount || 0), 0);
|
|
138
|
+
console.log(`\n ${sessions.length} quarantine session${sessions.length === 1 ? '' : 's'} found (${formatBytes(totalSize)} total)\n`);
|
|
139
|
+
|
|
140
|
+
const ok = await confirmPrompt(` Permanently delete all quarantined files? (y/N) `);
|
|
141
|
+
if (!ok) {
|
|
142
|
+
console.log(' Cancelled.\n');
|
|
143
|
+
process.exit(0);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let purged = 0;
|
|
148
|
+
let totalFiles = 0;
|
|
149
|
+
let totalSize = 0;
|
|
150
|
+
|
|
151
|
+
if (opts.id) {
|
|
152
|
+
const result = purgeSession(projectPath, opts.id);
|
|
153
|
+
purged = 1;
|
|
154
|
+
totalFiles = result.purgedFiles;
|
|
155
|
+
} else {
|
|
156
|
+
for (const s of sessions) {
|
|
157
|
+
try {
|
|
158
|
+
purgeSession(projectPath, s.sessionId);
|
|
159
|
+
purged++;
|
|
160
|
+
totalFiles += s.fileCount || 0;
|
|
161
|
+
totalSize += s.totalSize || 0;
|
|
162
|
+
} catch { /* skip */ }
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
console.log(renderPurgeOutput({ purged, totalFiles, totalSize }, { noColor }));
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// ── init ─────────────────────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
program
|
|
172
|
+
.command('init')
|
|
173
|
+
.description('Create a .swynx-lite.json config file')
|
|
174
|
+
.action(async () => {
|
|
175
|
+
const configPath = join(resolve('.'), '.swynx-lite.json');
|
|
176
|
+
if (existsSync(configPath)) {
|
|
177
|
+
console.log('\n .swynx-lite.json already exists.\n');
|
|
178
|
+
process.exit(0);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const { generateConfigTemplate } = await import('./config.mjs');
|
|
182
|
+
writeFileSync(configPath, generateConfigTemplate() + '\n');
|
|
183
|
+
|
|
184
|
+
console.log('\n Created .swynx-lite.json');
|
|
185
|
+
console.log('');
|
|
186
|
+
console.log(' Edit this file to:');
|
|
187
|
+
console.log(' \u2022 Ignore specific paths or patterns');
|
|
188
|
+
console.log(' \u2022 Set CI thresholds');
|
|
189
|
+
console.log(' \u2022 Configure security scanning');
|
|
190
|
+
console.log('');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// ── --pro flag ───────────────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
program.option('--pro', 'Show Swynx Pro features');
|
|
196
|
+
|
|
197
|
+
program.on('option:pro', () => {
|
|
198
|
+
const d = process.env.NO_COLOR ? (t) => t : (t) => `\x1b[2m${t}\x1b[0m`;
|
|
199
|
+
const b = process.env.NO_COLOR ? (t) => t : (t) => `\x1b[1m${t}\x1b[0m`;
|
|
200
|
+
const c = process.env.NO_COLOR ? (t) => t : (t) => `\x1b[1;96m${t}\x1b[0m`;
|
|
201
|
+
|
|
202
|
+
console.log('');
|
|
203
|
+
console.log(` ${c('swynx lite')} ${d(`v${VERSION}`)} — Free forever`);
|
|
204
|
+
console.log('');
|
|
205
|
+
console.log(` ${b('Swynx Pro')} is built for engineering teams:`);
|
|
206
|
+
console.log(' \u2022 Web dashboard with historical trends');
|
|
207
|
+
console.log(' \u2022 Predictive intelligence (decay signals)');
|
|
208
|
+
console.log(' \u2022 Per-export dead code detection');
|
|
209
|
+
console.log(' \u2022 Emissions and waste analysis');
|
|
210
|
+
console.log(' \u2022 Dependency and license scanning');
|
|
211
|
+
console.log(' \u2022 SARIF, Markdown, and PDF reports');
|
|
212
|
+
console.log(' \u2022 Enterprise reporting suite');
|
|
213
|
+
console.log(' \u2022 Air-gapped deployment');
|
|
214
|
+
console.log(' \u2022 Priority support');
|
|
215
|
+
console.log('');
|
|
216
|
+
console.log(` From \u00a32,000/year \u00b7 https://swynx.io/pro`);
|
|
217
|
+
console.log('');
|
|
218
|
+
process.exit(0);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
function collect(val, arr) {
|
|
224
|
+
arr.push(val);
|
|
225
|
+
return arr;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function dim(text) {
|
|
229
|
+
return process.env.NO_COLOR ? text : `\x1b[2m${text}\x1b[0m`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function formatBytes(bytes) {
|
|
233
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
234
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
235
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function confirmPrompt(message) {
|
|
239
|
+
return new Promise((resolve) => {
|
|
240
|
+
process.stdout.write(message);
|
|
241
|
+
process.stdin.setEncoding('utf-8');
|
|
242
|
+
process.stdin.resume();
|
|
243
|
+
process.stdin.once('data', (data) => {
|
|
244
|
+
process.stdin.pause();
|
|
245
|
+
resolve(data.trim().toLowerCase() === 'y');
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ── Default action: scan when no command given ──────────────────────────────
|
|
251
|
+
|
|
252
|
+
// If no command provided, default to `scan .`
|
|
253
|
+
// This makes `npx swynx-lite` work like `npx knip`
|
|
254
|
+
const args = process.argv.slice(2);
|
|
255
|
+
const commands = ['scan', 'clean', 'restore', 'purge', 'init', 'help'];
|
|
256
|
+
const hasCommand = args.some(a => commands.includes(a));
|
|
257
|
+
const hasHelpOrVersion = args.some(a => ['-h', '--help', '-v', '--version', '--pro'].includes(a));
|
|
258
|
+
|
|
259
|
+
if (args.length === 0 || (!hasCommand && !hasHelpOrVersion)) {
|
|
260
|
+
// Inject 'scan' as default command, pass remaining args as flags
|
|
261
|
+
process.argv.splice(2, 0, 'scan');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
program.parse();
|
package/src/config.mjs
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// src/config.mjs
|
|
2
|
+
// Config file loading: .swynx-lite.json + .swynxignore
|
|
3
|
+
|
|
4
|
+
import { existsSync, readFileSync } from 'fs';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
|
|
7
|
+
const DEFAULTS = {
|
|
8
|
+
ignore: [],
|
|
9
|
+
ci: {
|
|
10
|
+
threshold: 0,
|
|
11
|
+
failOnSecurity: true,
|
|
12
|
+
securitySeverity: 'HIGH',
|
|
13
|
+
},
|
|
14
|
+
security: {
|
|
15
|
+
enabled: true,
|
|
16
|
+
},
|
|
17
|
+
clean: {
|
|
18
|
+
quarantine: true,
|
|
19
|
+
importClean: true,
|
|
20
|
+
barrelClean: true,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Load .swynx-lite.json from project root
|
|
26
|
+
*/
|
|
27
|
+
function loadConfigFile(projectPath) {
|
|
28
|
+
const configPath = join(projectPath, '.swynx-lite.json');
|
|
29
|
+
if (!existsSync(configPath)) return {};
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
33
|
+
} catch (e) {
|
|
34
|
+
console.error(` Warning: invalid .swynx-lite.json — ${e.message}`);
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Load .swynxignore from project root (gitignore-style lines)
|
|
41
|
+
*/
|
|
42
|
+
function loadIgnoreFile(projectPath) {
|
|
43
|
+
const ignorePath = join(projectPath, '.swynxignore');
|
|
44
|
+
if (!existsSync(ignorePath)) return [];
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const content = readFileSync(ignorePath, 'utf-8');
|
|
48
|
+
return content
|
|
49
|
+
.split('\n')
|
|
50
|
+
.map(line => line.trim())
|
|
51
|
+
.filter(line => line && !line.startsWith('#'));
|
|
52
|
+
} catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Load config, merging: defaults < .swynx-lite.json < .swynxignore < CLI options
|
|
59
|
+
*/
|
|
60
|
+
export function loadConfig(projectPath, cliOptions = {}) {
|
|
61
|
+
const fileConfig = loadConfigFile(projectPath);
|
|
62
|
+
const ignorePatterns = loadIgnoreFile(projectPath);
|
|
63
|
+
|
|
64
|
+
// Merge ignore patterns: file config + .swynxignore + CLI --ignore
|
|
65
|
+
const ignore = [
|
|
66
|
+
...(DEFAULTS.ignore),
|
|
67
|
+
...(fileConfig.ignore || []),
|
|
68
|
+
...ignorePatterns,
|
|
69
|
+
...(cliOptions.ignore || []),
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const config = {
|
|
73
|
+
ignore,
|
|
74
|
+
ci: {
|
|
75
|
+
...DEFAULTS.ci,
|
|
76
|
+
...(fileConfig.ci || {}),
|
|
77
|
+
},
|
|
78
|
+
security: {
|
|
79
|
+
...DEFAULTS.security,
|
|
80
|
+
...(fileConfig.security || {}),
|
|
81
|
+
},
|
|
82
|
+
clean: {
|
|
83
|
+
...DEFAULTS.clean,
|
|
84
|
+
...(fileConfig.clean || {}),
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// CLI overrides
|
|
89
|
+
if (cliOptions.threshold !== undefined) config.ci.threshold = cliOptions.threshold;
|
|
90
|
+
if (cliOptions.security === false) config.security.enabled = false;
|
|
91
|
+
|
|
92
|
+
return config;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Generate a default .swynx-lite.json template
|
|
97
|
+
*/
|
|
98
|
+
export function generateConfigTemplate() {
|
|
99
|
+
return JSON.stringify({
|
|
100
|
+
ignore: [
|
|
101
|
+
'**/__tests__/**',
|
|
102
|
+
'**/*.test.*',
|
|
103
|
+
'**/*.spec.*',
|
|
104
|
+
'scripts/**',
|
|
105
|
+
'docs/**',
|
|
106
|
+
],
|
|
107
|
+
ci: {
|
|
108
|
+
threshold: 5,
|
|
109
|
+
failOnSecurity: true,
|
|
110
|
+
securitySeverity: 'HIGH',
|
|
111
|
+
},
|
|
112
|
+
security: {
|
|
113
|
+
enabled: true,
|
|
114
|
+
},
|
|
115
|
+
clean: {
|
|
116
|
+
quarantine: true,
|
|
117
|
+
importClean: true,
|
|
118
|
+
barrelClean: true,
|
|
119
|
+
},
|
|
120
|
+
}, null, 2);
|
|
121
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
// src/output/console.mjs
|
|
2
|
+
// Terminal output formatter — raw ANSI codes (no chalk)
|
|
3
|
+
|
|
4
|
+
const ESC = '\x1b[';
|
|
5
|
+
const RESET = `${ESC}0m`;
|
|
6
|
+
const BOLD = `${ESC}1m`;
|
|
7
|
+
const DIM = `${ESC}2m`;
|
|
8
|
+
const RED = `${ESC}31m`;
|
|
9
|
+
const GREEN = `${ESC}32m`;
|
|
10
|
+
const YELLOW = `${ESC}33m`;
|
|
11
|
+
const CYAN = `${ESC}36m`;
|
|
12
|
+
const WHITE = `${ESC}37m`;
|
|
13
|
+
const BRIGHT_RED = `${ESC}91m`;
|
|
14
|
+
const BRIGHT_GREEN = `${ESC}92m`;
|
|
15
|
+
const BRIGHT_YELLOW = `${ESC}93m`;
|
|
16
|
+
const BRIGHT_CYAN = `${ESC}96m`;
|
|
17
|
+
|
|
18
|
+
function c(style, text, noColor) {
|
|
19
|
+
if (noColor) return text;
|
|
20
|
+
return `${style}${text}${RESET}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function formatBytes(bytes) {
|
|
24
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
25
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
26
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function formatNumber(n) {
|
|
30
|
+
return n.toLocaleString('en-US');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function pad(str, len) {
|
|
34
|
+
return str + ' '.repeat(Math.max(0, len - str.length));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function rpad(str, len) {
|
|
38
|
+
return ' '.repeat(Math.max(0, len - str.length)) + str;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Render scan results to the console
|
|
43
|
+
*/
|
|
44
|
+
export function renderScanOutput(results, options = {}) {
|
|
45
|
+
const { noColor = false, verbose = false, ci = false, security = null } = options;
|
|
46
|
+
const lines = [];
|
|
47
|
+
|
|
48
|
+
const nc = noColor;
|
|
49
|
+
const hr = '─'.repeat(48);
|
|
50
|
+
|
|
51
|
+
// ── Header ──
|
|
52
|
+
lines.push('');
|
|
53
|
+
lines.push(` ${c(BOLD + BRIGHT_CYAN, 'swynx lite', nc)} ${c(DIM, `v1.0.0`, nc)}`);
|
|
54
|
+
lines.push('');
|
|
55
|
+
|
|
56
|
+
// ── Summary ──
|
|
57
|
+
lines.push(` ${c(DIM, '──', nc)} ${c(BOLD, 'Summary', nc)} ${c(DIM, hr, nc)}`);
|
|
58
|
+
lines.push('');
|
|
59
|
+
|
|
60
|
+
const summary = results.summary;
|
|
61
|
+
|
|
62
|
+
const summaryRows = [
|
|
63
|
+
['Files scanned', formatNumber(summary.totalFiles)],
|
|
64
|
+
['Entry points', formatNumber(summary.entryPoints)],
|
|
65
|
+
['Reachable', formatNumber(summary.reachableFiles)],
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
// Dead files — highlight red if > 0
|
|
69
|
+
const deadLabel = 'Dead files';
|
|
70
|
+
const deadCount = summary.deadFiles;
|
|
71
|
+
const deadRate = summary.deadRate;
|
|
72
|
+
const deadValue = deadCount > 0
|
|
73
|
+
? `${c(BRIGHT_RED, formatNumber(deadCount), nc)} ${c(DIM, `(${deadRate})`, nc)}`
|
|
74
|
+
: `${c(BRIGHT_GREEN, '0', nc)}`;
|
|
75
|
+
summaryRows.push([deadLabel, deadValue]);
|
|
76
|
+
|
|
77
|
+
// Dead size
|
|
78
|
+
if (summary.totalDeadBytes > 0) {
|
|
79
|
+
summaryRows.push(['Dead code size', formatBytes(summary.totalDeadBytes)]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const [label, value] of summaryRows) {
|
|
83
|
+
lines.push(` ${c(DIM, pad(label, 18), nc)}${value}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
lines.push('');
|
|
87
|
+
|
|
88
|
+
// ── Dead Files ──
|
|
89
|
+
if (deadCount > 0) {
|
|
90
|
+
lines.push(` ${c(DIM, '──', nc)} ${c(BOLD, 'Dead Files', nc)} ${c(DIM, hr, nc)}`);
|
|
91
|
+
lines.push('');
|
|
92
|
+
|
|
93
|
+
const deadFiles = results.deadFiles || [];
|
|
94
|
+
const showCount = verbose ? deadFiles.length : Math.min(5, deadFiles.length);
|
|
95
|
+
|
|
96
|
+
for (let i = 0; i < showCount; i++) {
|
|
97
|
+
const f = deadFiles[i];
|
|
98
|
+
const path = f.file || f.path || '';
|
|
99
|
+
const size = formatBytes(f.size);
|
|
100
|
+
const lineCount = f.lines ? `${formatNumber(f.lines)} lines` : '';
|
|
101
|
+
lines.push(` ${c(WHITE, pad(path, 42), nc)} ${c(DIM, rpad(size, 10), nc)} ${c(DIM, lineCount, nc)}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!verbose && deadFiles.length > 5) {
|
|
105
|
+
lines.push(` ${c(DIM, `... and ${deadFiles.length - 5} more`, nc)}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
lines.push('');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── Security ──
|
|
112
|
+
if (security && security.summary && security.summary.total > 0) {
|
|
113
|
+
lines.push(` ${c(DIM, '──', nc)} ${c(BOLD, 'Security', nc)} ${c(DIM, hr, nc)}`);
|
|
114
|
+
lines.push('');
|
|
115
|
+
|
|
116
|
+
const secSummary = security.summary;
|
|
117
|
+
const inDeadLabel = secSummary.inDeadCode > 0
|
|
118
|
+
? `${secSummary.inDeadCode} finding${secSummary.inDeadCode === 1 ? '' : 's'} in dead code`
|
|
119
|
+
: '';
|
|
120
|
+
const inLiveLabel = secSummary.inLiveCode > 0
|
|
121
|
+
? `${secSummary.inLiveCode} finding${secSummary.inLiveCode === 1 ? '' : 's'} in live code`
|
|
122
|
+
: '';
|
|
123
|
+
if (inDeadLabel) lines.push(` ${c(YELLOW, inDeadLabel, nc)}`);
|
|
124
|
+
if (inLiveLabel) lines.push(` ${c(RED, inLiveLabel, nc)}`);
|
|
125
|
+
lines.push('');
|
|
126
|
+
|
|
127
|
+
// Show top findings
|
|
128
|
+
const findings = security.findings || [];
|
|
129
|
+
const showSecCount = verbose ? findings.length : Math.min(5, findings.length);
|
|
130
|
+
|
|
131
|
+
for (let i = 0; i < showSecCount; i++) {
|
|
132
|
+
const f = findings[i];
|
|
133
|
+
const sevColor = f.severity === 'CRITICAL' ? BRIGHT_RED
|
|
134
|
+
: f.severity === 'HIGH' ? RED
|
|
135
|
+
: f.severity === 'MEDIUM' ? YELLOW
|
|
136
|
+
: DIM;
|
|
137
|
+
|
|
138
|
+
lines.push(` ${c(BOLD + sevColor, pad(f.severity, 10), nc)}${c(WHITE, f.file, nc)}${c(DIM, `:${f.line}`, nc)}`);
|
|
139
|
+
lines.push(` ${c(DIM, `${f.cwe} ${f.cweName}`, nc)} ${c(DIM, '—', nc)} ${f.description}`);
|
|
140
|
+
if (f.risk) {
|
|
141
|
+
lines.push(` ${c(DIM, f.risk, nc)}`);
|
|
142
|
+
}
|
|
143
|
+
lines.push('');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!verbose && findings.length > 5) {
|
|
147
|
+
lines.push(` ${c(DIM, `... and ${findings.length - 5} more findings`, nc)}`);
|
|
148
|
+
lines.push('');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── What Next ── (interactive only)
|
|
153
|
+
if (!ci) {
|
|
154
|
+
if (deadCount > 0) {
|
|
155
|
+
lines.push(` ${c(DIM, '──', nc)} ${c(BOLD, 'What Next', nc)} ${c(DIM, hr, nc)}`);
|
|
156
|
+
lines.push('');
|
|
157
|
+
lines.push(` Run ${c(BRIGHT_CYAN, 'swynx-lite clean', nc)} to remove ${formatNumber(deadCount)} dead file${deadCount === 1 ? '' : 's'} ${c(DIM, `(saves ${formatBytes(summary.totalDeadBytes)})`, nc)}`);
|
|
158
|
+
lines.push(` Run ${c(BRIGHT_CYAN, 'swynx-lite scan --json', nc)} for machine-readable output`);
|
|
159
|
+
lines.push('');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Pro footer — dim, unobtrusive
|
|
163
|
+
lines.push(` ${c(DIM, 'Dashboard, predictions, and more → swynx.io/pro', nc)}`);
|
|
164
|
+
lines.push('');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return lines.join('\n');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Render clean results to the console
|
|
172
|
+
*/
|
|
173
|
+
export function renderCleanOutput(results, options = {}) {
|
|
174
|
+
const { noColor = false, dryRun = false } = options;
|
|
175
|
+
const lines = [];
|
|
176
|
+
const nc = noColor;
|
|
177
|
+
|
|
178
|
+
lines.push('');
|
|
179
|
+
lines.push(` ${c(BOLD + BRIGHT_CYAN, 'swynx lite', nc)} ${c(DIM, 'v1.0.0', nc)}${dryRun ? c(DIM, ' — dry run (no files will be modified)', nc) : ''}`);
|
|
180
|
+
lines.push('');
|
|
181
|
+
|
|
182
|
+
if (dryRun) {
|
|
183
|
+
lines.push(` Would remove ${c(BOLD, formatNumber(results.filesRemoved), nc)} file${results.filesRemoved === 1 ? '' : 's'} ${c(DIM, `(${formatBytes(results.bytesRemoved)})`, nc)}:`);
|
|
184
|
+
} else {
|
|
185
|
+
lines.push(` ${formatNumber(results.deadCount || 0)} dead files found ${c(DIM, `(${formatBytes(results.bytesRemoved)})`, nc)}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
lines.push('');
|
|
189
|
+
|
|
190
|
+
// File list
|
|
191
|
+
const files = results.files || [];
|
|
192
|
+
const showCount = Math.min(5, files.length);
|
|
193
|
+
|
|
194
|
+
for (let i = 0; i < showCount; i++) {
|
|
195
|
+
const f = files[i];
|
|
196
|
+
const path = typeof f === 'string' ? f : (f.file || f.path || '');
|
|
197
|
+
const size = typeof f === 'object' && f.size ? formatBytes(f.size) : '';
|
|
198
|
+
lines.push(` ${c(WHITE, pad(path, 42), nc)} ${c(DIM, rpad(size, 10), nc)}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (files.length > 5) {
|
|
202
|
+
lines.push(` ${c(DIM, `... and ${files.length - 5} more`, nc)}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
lines.push('');
|
|
206
|
+
|
|
207
|
+
if (results.importsRemoved > 0 || results.barrelExportsRemoved > 0) {
|
|
208
|
+
if (dryRun) {
|
|
209
|
+
lines.push(` Would clean:`);
|
|
210
|
+
} else {
|
|
211
|
+
lines.push(` Also cleaned:`);
|
|
212
|
+
}
|
|
213
|
+
if (results.importsRemoved > 0) {
|
|
214
|
+
lines.push(` ${formatNumber(results.importsRemoved)} dead import${results.importsRemoved === 1 ? '' : 's'} from live files`);
|
|
215
|
+
}
|
|
216
|
+
if (results.barrelExportsRemoved > 0) {
|
|
217
|
+
lines.push(` ${formatNumber(results.barrelExportsRemoved)} dead re-export${results.barrelExportsRemoved === 1 ? '' : 's'} from barrel files`);
|
|
218
|
+
}
|
|
219
|
+
lines.push('');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!dryRun && results.sessionId) {
|
|
223
|
+
lines.push(` ${c(BRIGHT_GREEN, '\u2713', nc)} Quarantined ${formatNumber(results.filesRemoved)} file${results.filesRemoved === 1 ? '' : 's'} to .swynx-quarantine/`);
|
|
224
|
+
if (results.importsRemoved > 0 || results.barrelExportsRemoved > 0) {
|
|
225
|
+
lines.push(` ${c(BRIGHT_GREEN, '\u2713', nc)} Cleaned ${formatNumber(results.importsRemoved)} import${results.importsRemoved === 1 ? '' : 's'}, ${formatNumber(results.barrelExportsRemoved)} barrel export${results.barrelExportsRemoved === 1 ? '' : 's'}`);
|
|
226
|
+
}
|
|
227
|
+
lines.push(` ${c(BRIGHT_GREEN, '\u2713', nc)} Removed ${formatBytes(results.bytesRemoved)} of dead code`);
|
|
228
|
+
lines.push('');
|
|
229
|
+
lines.push(` Undo with ${c(BRIGHT_CYAN, 'swynx-lite restore', nc)}`);
|
|
230
|
+
lines.push(` Finalise with ${c(BRIGHT_CYAN, 'swynx-lite purge', nc)}`);
|
|
231
|
+
lines.push('');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return lines.join('\n');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Render restore result
|
|
239
|
+
*/
|
|
240
|
+
export function renderRestoreOutput(result, options = {}) {
|
|
241
|
+
const nc = options.noColor || false;
|
|
242
|
+
const lines = [];
|
|
243
|
+
|
|
244
|
+
lines.push('');
|
|
245
|
+
if (result.restored && result.restored.length > 0) {
|
|
246
|
+
lines.push(` ${c(BRIGHT_GREEN, '\u2713', nc)} Restored ${result.restored.length} file${result.restored.length === 1 ? '' : 's'} from quarantine session ${c(DIM, result.sessionId, nc)}`);
|
|
247
|
+
lines.push('');
|
|
248
|
+
lines.push(` Restored files are back in their original locations.`);
|
|
249
|
+
lines.push(` Quarantine session kept — run ${c(BRIGHT_CYAN, 'swynx-lite purge', nc)} to clean up.`);
|
|
250
|
+
} else {
|
|
251
|
+
lines.push(` No files to restore.`);
|
|
252
|
+
}
|
|
253
|
+
lines.push('');
|
|
254
|
+
|
|
255
|
+
return lines.join('\n');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Render session list
|
|
260
|
+
*/
|
|
261
|
+
export function renderSessionList(sessions, options = {}) {
|
|
262
|
+
const nc = options.noColor || false;
|
|
263
|
+
const lines = [];
|
|
264
|
+
|
|
265
|
+
lines.push('');
|
|
266
|
+
if (sessions.length === 0) {
|
|
267
|
+
lines.push(` No quarantine sessions found.`);
|
|
268
|
+
} else {
|
|
269
|
+
lines.push(` ${sessions.length} quarantine session${sessions.length === 1 ? '' : 's'}:`);
|
|
270
|
+
lines.push('');
|
|
271
|
+
for (const s of sessions) {
|
|
272
|
+
const date = new Date(s.createdAt).toLocaleString();
|
|
273
|
+
const status = s.status === 'restored' ? c(YELLOW, '[restored]', nc) : c(GREEN, '[active]', nc);
|
|
274
|
+
lines.push(` ${c(DIM, s.sessionId, nc)} ${pad(date, 22)} ${formatNumber(s.fileCount)} file${s.fileCount === 1 ? '' : 's'} ${formatBytes(s.totalSize)} ${status}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
lines.push('');
|
|
278
|
+
|
|
279
|
+
return lines.join('\n');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Render purge result
|
|
284
|
+
*/
|
|
285
|
+
export function renderPurgeOutput(results, options = {}) {
|
|
286
|
+
const nc = options.noColor || false;
|
|
287
|
+
const lines = [];
|
|
288
|
+
|
|
289
|
+
lines.push('');
|
|
290
|
+
if (results.purged > 0) {
|
|
291
|
+
lines.push(` ${c(BRIGHT_GREEN, '\u2713', nc)} Purged ${results.purged} session${results.purged === 1 ? '' : 's'} (${formatNumber(results.totalFiles)} file${results.totalFiles === 1 ? '' : 's'}, ${formatBytes(results.totalSize)})`);
|
|
292
|
+
} else {
|
|
293
|
+
lines.push(` No quarantine sessions to purge.`);
|
|
294
|
+
}
|
|
295
|
+
lines.push('');
|
|
296
|
+
|
|
297
|
+
return lines.join('\n');
|
|
298
|
+
}
|