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/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
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
|
+
}
|