@hunchoweb/nclean 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +260 -2
- package/dist/cleanup/remove.d.ts +2 -0
- package/dist/cleanup/remove.js +29 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +55 -0
- package/dist/commands/clean.d.ts +2 -0
- package/dist/commands/clean.js +67 -0
- package/dist/commands/scan.d.ts +2 -0
- package/dist/commands/scan.js +44 -0
- package/dist/scanner/activity.d.ts +2 -0
- package/dist/scanner/activity.js +54 -0
- package/dist/scanner/discover.d.ts +2 -0
- package/dist/scanner/discover.js +68 -0
- package/dist/scanner/size.d.ts +1 -0
- package/dist/scanner/size.js +29 -0
- package/dist/types.d.ts +30 -0
- package/dist/types.js +1 -0
- package/dist/ui/diagnostics.d.ts +2 -0
- package/dist/ui/diagnostics.js +27 -0
- package/dist/ui/interactive.d.ts +10 -0
- package/dist/ui/interactive.js +88 -0
- package/dist/ui/layout.d.ts +14 -0
- package/dist/ui/layout.js +128 -0
- package/dist/ui/output.d.ts +13 -0
- package/dist/ui/output.js +72 -0
- package/dist/ui/progress.d.ts +11 -0
- package/dist/ui/progress.js +75 -0
- package/dist/ui/typography.d.ts +2 -0
- package/dist/ui/typography.js +40 -0
- package/dist/utils/format.d.ts +6 -0
- package/dist/utils/format.js +24 -0
- package/dist/utils/package-manager.d.ts +3 -0
- package/dist/utils/package-manager.js +18 -0
- package/dist/utils/paths.d.ts +5 -0
- package/dist/utils/paths.js +50 -0
- package/dist/utils/project-scope.d.ts +2 -0
- package/dist/utils/project-scope.js +26 -0
- package/package.json +58 -4
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import { lstat, realpath } from 'node:fs/promises';
|
|
4
|
+
import { terminalText } from './format.js';
|
|
5
|
+
import { assertProjectScope } from './project-scope.js';
|
|
6
|
+
export function expandPath(input) {
|
|
7
|
+
if (input === '~')
|
|
8
|
+
return os.homedir();
|
|
9
|
+
return path.resolve(input.startsWith('~/') || input.startsWith('~\\') ? path.join(os.homedir(), input.slice(2)) : input);
|
|
10
|
+
}
|
|
11
|
+
export function within(root, target) {
|
|
12
|
+
const relative = path.relative(root, target);
|
|
13
|
+
return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
14
|
+
}
|
|
15
|
+
export function displayPath(value) {
|
|
16
|
+
const home = os.homedir();
|
|
17
|
+
return terminalText(within(home, value) ? `~${value.slice(home.length)}` : value);
|
|
18
|
+
}
|
|
19
|
+
export async function scanRoots(inputs) {
|
|
20
|
+
const roots = [];
|
|
21
|
+
for (const input of inputs.length ? inputs : [process.cwd()]) {
|
|
22
|
+
const resolved = expandPath(input);
|
|
23
|
+
const stat = await lstat(resolved);
|
|
24
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
25
|
+
throw new Error(`Scan root must be a real directory: ${displayPath(resolved)}`);
|
|
26
|
+
const canonical = await realpath(resolved);
|
|
27
|
+
if (canonical === path.parse(canonical).root)
|
|
28
|
+
throw new Error('Scanning the filesystem root is not supported. Choose a project directory.');
|
|
29
|
+
if (canonical.split(path.sep).includes('node_modules'))
|
|
30
|
+
throw new Error('Choose a project directory, not a directory inside node_modules.');
|
|
31
|
+
assertProjectScope(canonical);
|
|
32
|
+
roots.push(canonical);
|
|
33
|
+
}
|
|
34
|
+
return [...new Set(roots)].filter(root => !roots.some(other => other !== root && within(other, root)));
|
|
35
|
+
}
|
|
36
|
+
export async function assertRealPath(root, target) {
|
|
37
|
+
if (!within(root, target))
|
|
38
|
+
throw new Error('Path is outside the scan root.');
|
|
39
|
+
let current = root;
|
|
40
|
+
const parts = path.relative(root, target).split(path.sep).filter(Boolean);
|
|
41
|
+
for (const part of ['', ...parts]) {
|
|
42
|
+
if (part)
|
|
43
|
+
current = path.join(current, part);
|
|
44
|
+
const stat = await lstat(current);
|
|
45
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
46
|
+
throw new Error(`Directory changed or is a symlink: ${displayPath(current)}`);
|
|
47
|
+
}
|
|
48
|
+
if (await realpath(target) !== target)
|
|
49
|
+
throw new Error('Path resolves through a symlink.');
|
|
50
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
// Apply to the full path so choosing a protected subdirectory explicitly
|
|
3
|
+
// cannot bypass the same exclusions used during recursive discovery.
|
|
4
|
+
const generated = new Set(['node_modules', 'dist', 'build', 'coverage', 'vendor']);
|
|
5
|
+
const managed = new Set(['library', 'applications', 'appdata', 'program files', 'program files (x86)', 'programdata', 'windows']);
|
|
6
|
+
const systemRoots = ['/usr', '/opt', '/System', '/Applications', '/Library'];
|
|
7
|
+
export function projectScopeReason(folder) {
|
|
8
|
+
const absolute = path.resolve(folder);
|
|
9
|
+
if (process.platform !== 'win32' && systemRoots.some(root => absolute === root || absolute.startsWith(`${root}/`))) {
|
|
10
|
+
return 'system or installed-software directory';
|
|
11
|
+
}
|
|
12
|
+
for (const part of absolute.split(path.sep).filter(Boolean)) {
|
|
13
|
+
if (part.startsWith('.'))
|
|
14
|
+
return 'hidden or tool-managed directory';
|
|
15
|
+
if (generated.has(part.toLowerCase()))
|
|
16
|
+
return 'dependency or generated directory';
|
|
17
|
+
if (managed.has(part.toLowerCase()) || /\.(?:app|framework|bundle)$/i.test(part))
|
|
18
|
+
return 'application or tool-managed directory';
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
export function assertProjectScope(folder) {
|
|
23
|
+
const reason = projectScopeReason(folder);
|
|
24
|
+
if (reason)
|
|
25
|
+
throw new Error(`Refusing project cleanup in a ${reason}. Choose a source project directory.`);
|
|
26
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,60 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hunchoweb/nclean",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Clean up forgotten Node.js dependencies.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"nclean": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=22"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"dev": "node --import tsx src/cli.ts",
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"test": "npm run build && node --import tsx --test test/*.test.ts",
|
|
21
|
+
"lint": "tsc --noEmit",
|
|
22
|
+
"prepack": "npm run build",
|
|
23
|
+
"prepublishOnly": "npm test && npm run lint",
|
|
24
|
+
"demo:setup": "node scripts/demo.mjs"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/hunchoweb/nodeprune.git"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/hunchoweb/nodeprune#readme",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/hunchoweb/nodeprune/issues"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public",
|
|
36
|
+
"registry": "https://registry.npmjs.org/"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"cli",
|
|
40
|
+
"node_modules",
|
|
41
|
+
"cleanup",
|
|
42
|
+
"disk-space",
|
|
43
|
+
"node",
|
|
44
|
+
"npm",
|
|
45
|
+
"pnpm"
|
|
46
|
+
],
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@inquirer/core": "^10.3.2",
|
|
50
|
+
"commander": "^14.0.0",
|
|
51
|
+
"ora": "^8.2.0",
|
|
52
|
+
"picocolors": "^1.1.1",
|
|
53
|
+
"string-width": "^7.2.0"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/node": "^22.0.0",
|
|
57
|
+
"tsx": "^4.20.0",
|
|
58
|
+
"typescript": "^5.9.0"
|
|
59
|
+
}
|
|
60
|
+
}
|