packagepurge 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/LICENSE +21 -0
- package/README.md +125 -0
- package/core/Cargo.lock +1093 -0
- package/core/Cargo.toml +22 -0
- package/core/src/arc_lfu.rs +91 -0
- package/core/src/cache.rs +205 -0
- package/core/src/lockfiles.rs +112 -0
- package/core/src/main.rs +125 -0
- package/core/src/ml.rs +188 -0
- package/core/src/optimization.rs +314 -0
- package/core/src/safety.rs +103 -0
- package/core/src/scanner.rs +136 -0
- package/core/src/symlink.rs +223 -0
- package/core/src/types.rs +87 -0
- package/core/src/usage_tracker.rs +107 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +249 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/bindings.d.ts +33 -0
- package/dist/core/bindings.d.ts.map +1 -0
- package/dist/core/bindings.js +172 -0
- package/dist/core/bindings.js.map +1 -0
- package/dist/managers/base-manager.d.ts +33 -0
- package/dist/managers/base-manager.d.ts.map +1 -0
- package/dist/managers/base-manager.js +122 -0
- package/dist/managers/base-manager.js.map +1 -0
- package/dist/managers/index.d.ts +12 -0
- package/dist/managers/index.d.ts.map +1 -0
- package/dist/managers/index.js +37 -0
- package/dist/managers/index.js.map +1 -0
- package/dist/managers/npm-manager.d.ts +14 -0
- package/dist/managers/npm-manager.d.ts.map +1 -0
- package/dist/managers/npm-manager.js +128 -0
- package/dist/managers/npm-manager.js.map +1 -0
- package/dist/managers/pnpm-manager.d.ts +14 -0
- package/dist/managers/pnpm-manager.d.ts.map +1 -0
- package/dist/managers/pnpm-manager.js +137 -0
- package/dist/managers/pnpm-manager.js.map +1 -0
- package/dist/managers/yarn-manager.d.ts +14 -0
- package/dist/managers/yarn-manager.d.ts.map +1 -0
- package/dist/managers/yarn-manager.js +141 -0
- package/dist/managers/yarn-manager.js.map +1 -0
- package/dist/types/index.d.ts +85 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +13 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/logger.d.ts +18 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +50 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +64 -0
- package/src/cli/index.ts +212 -0
- package/src/core/bindings.ts +157 -0
- package/src/managers/base-manager.ts +117 -0
- package/src/managers/index.ts +32 -0
- package/src/managers/npm-manager.ts +96 -0
- package/src/managers/pnpm-manager.ts +107 -0
- package/src/managers/yarn-manager.ts +112 -0
- package/src/types/index.ts +97 -0
- package/src/utils/logger.ts +50 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAKA,oBAAY,QAAQ;IAClB,KAAK,IAAI;IACT,IAAI,IAAI;IACR,IAAI,IAAI;IACR,KAAK,IAAI;CACV;AAED,cAAM,MAAM;IACV,OAAO,CAAC,KAAK,CAA2B;IAExC,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI;IAI/B,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI;IAM5C,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI;IAM3C,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI;IAM3C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI;IAM5C,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI;CAG/C;AAED,eAAO,MAAM,MAAM,QAAe,CAAC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.logger = exports.LogLevel = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* Logger utility for PackagePurge
|
|
9
|
+
*/
|
|
10
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
11
|
+
var LogLevel;
|
|
12
|
+
(function (LogLevel) {
|
|
13
|
+
LogLevel[LogLevel["DEBUG"] = 0] = "DEBUG";
|
|
14
|
+
LogLevel[LogLevel["INFO"] = 1] = "INFO";
|
|
15
|
+
LogLevel[LogLevel["WARN"] = 2] = "WARN";
|
|
16
|
+
LogLevel[LogLevel["ERROR"] = 3] = "ERROR";
|
|
17
|
+
})(LogLevel || (exports.LogLevel = LogLevel = {}));
|
|
18
|
+
class Logger {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.level = LogLevel.INFO;
|
|
21
|
+
}
|
|
22
|
+
setLevel(level) {
|
|
23
|
+
this.level = level;
|
|
24
|
+
}
|
|
25
|
+
debug(message, ...args) {
|
|
26
|
+
if (this.level <= LogLevel.DEBUG) {
|
|
27
|
+
console.log(chalk_1.default.gray(`[DEBUG] ${message}`), ...args);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
info(message, ...args) {
|
|
31
|
+
if (this.level <= LogLevel.INFO) {
|
|
32
|
+
console.log(chalk_1.default.blue(`[INFO] ${message}`), ...args);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
warn(message, ...args) {
|
|
36
|
+
if (this.level <= LogLevel.WARN) {
|
|
37
|
+
console.warn(chalk_1.default.yellow(`[WARN] ${message}`), ...args);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
error(message, ...args) {
|
|
41
|
+
if (this.level <= LogLevel.ERROR) {
|
|
42
|
+
console.error(chalk_1.default.red(`[ERROR] ${message}`), ...args);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
success(message, ...args) {
|
|
46
|
+
console.log(chalk_1.default.green(`[SUCCESS] ${message}`), ...args);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.logger = new Logger();
|
|
50
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":";;;;;;AAAA;;GAEG;AACH,kDAA0B;AAE1B,IAAY,QAKX;AALD,WAAY,QAAQ;IAClB,yCAAS,CAAA;IACT,uCAAQ,CAAA;IACR,uCAAQ,CAAA;IACR,yCAAS,CAAA;AACX,CAAC,EALW,QAAQ,wBAAR,QAAQ,QAKnB;AAED,MAAM,MAAM;IAAZ;QACU,UAAK,GAAa,QAAQ,CAAC,IAAI,CAAC;IAiC1C,CAAC;IA/BC,QAAQ,CAAC,KAAe;QACtB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACrB,CAAC;IAED,KAAK,CAAC,OAAe,EAAE,GAAG,IAAW;QACnC,IAAI,IAAI,CAAC,KAAK,IAAI,QAAQ,CAAC,KAAK,EAAE,CAAC;YACjC,OAAO,CAAC,GAAG,CAAC,eAAK,CAAC,IAAI,CAAC,WAAW,OAAO,EAAE,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC;QACzD,CAAC;IACH,CAAC;IAED,IAAI,CAAC,OAAe,EAAE,GAAG,IAAW;QAClC,IAAI,IAAI,CAAC,KAAK,IAAI,QAAQ,CAAC,IAAI,EAAE,CAAC;YAChC,OAAO,CAAC,GAAG,CAAC,eAAK,CAAC,IAAI,CAAC,UAAU,OAAO,EAAE,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC;QACxD,CAAC;IACH,CAAC;IAED,IAAI,CAAC,OAAe,EAAE,GAAG,IAAW;QAClC,IAAI,IAAI,CAAC,KAAK,IAAI,QAAQ,CAAC,IAAI,EAAE,CAAC;YAChC,OAAO,CAAC,IAAI,CAAC,eAAK,CAAC,MAAM,CAAC,UAAU,OAAO,EAAE,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC;QAC3D,CAAC;IACH,CAAC;IAED,KAAK,CAAC,OAAe,EAAE,GAAG,IAAW;QACnC,IAAI,IAAI,CAAC,KAAK,IAAI,QAAQ,CAAC,KAAK,EAAE,CAAC;YACjC,OAAO,CAAC,KAAK,CAAC,eAAK,CAAC,GAAG,CAAC,WAAW,OAAO,EAAE,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC;QAC1D,CAAC;IACH,CAAC;IAED,OAAO,CAAC,OAAe,EAAE,GAAG,IAAW;QACrC,OAAO,CAAC,GAAG,CAAC,eAAK,CAAC,KAAK,CAAC,aAAa,OAAO,EAAE,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC;IAC5D,CAAC;CACF;AAEY,QAAA,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "packagepurge",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Intelligent package manager cache cleanup service with project-aware optimization",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"packagepurge": "dist/cli/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "npm run build:core && tsc",
|
|
12
|
+
"build:core": "cd core && cargo build --release",
|
|
13
|
+
"build:core:debug": "cd core && cargo build",
|
|
14
|
+
"dev": "npm run build:core:debug && ts-node src/cli/index.ts",
|
|
15
|
+
"start": "node dist/cli/index.js",
|
|
16
|
+
"test": "jest",
|
|
17
|
+
"lint": "eslint src/**/*.ts",
|
|
18
|
+
"clean": "rimraf dist",
|
|
19
|
+
"clean:core": "cd core && cargo clean"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"npm",
|
|
23
|
+
"yarn",
|
|
24
|
+
"pnpm",
|
|
25
|
+
"cache",
|
|
26
|
+
"cleanup",
|
|
27
|
+
"optimization",
|
|
28
|
+
"storage",
|
|
29
|
+
"deduplication"
|
|
30
|
+
],
|
|
31
|
+
"author": "Eng. Onyango Benard",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"commander": "^11.1.0",
|
|
35
|
+
"chalk": "^4.1.2",
|
|
36
|
+
"inquirer": "^8.2.6",
|
|
37
|
+
"fs-extra": "^11.2.0",
|
|
38
|
+
"tar": "^6.2.1",
|
|
39
|
+
"glob": "^10.3.10",
|
|
40
|
+
"semver": "^7.5.4",
|
|
41
|
+
"@types/node": "^20.10.6",
|
|
42
|
+
"node-machine-id": "^1.1.12",
|
|
43
|
+
"yaml": "^2.4.5"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/fs-extra": "^11.0.4",
|
|
47
|
+
"@types/inquirer": "^8.2.10",
|
|
48
|
+
"@types/node": "^20.10.6",
|
|
49
|
+
"@types/semver": "^7.5.4",
|
|
50
|
+
"@typescript-eslint/eslint-plugin": "^6.17.0",
|
|
51
|
+
"@typescript-eslint/parser": "^6.17.0",
|
|
52
|
+
"eslint": "^8.56.0",
|
|
53
|
+
"jest": "^29.7.0",
|
|
54
|
+
"@types/jest": "^29.5.11",
|
|
55
|
+
"ts-jest": "^29.1.1",
|
|
56
|
+
"ts-node": "^10.9.2",
|
|
57
|
+
"typescript": "^5.3.3",
|
|
58
|
+
"rimraf": "^5.0.5"
|
|
59
|
+
},
|
|
60
|
+
"engines": {
|
|
61
|
+
"node": ">=16.0.0"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import { logger } from '../utils/logger';
|
|
6
|
+
import YAML from 'yaml';
|
|
7
|
+
import { optimize, executeSymlinking } from '../core/bindings';
|
|
8
|
+
|
|
9
|
+
function isWindows(): boolean {
|
|
10
|
+
return process.platform === 'win32';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function fileExists(p: string): boolean {
|
|
14
|
+
try { return fs.existsSync(p); } catch { return false; }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function resolveFromPATH(name: string): string | null {
|
|
18
|
+
const exts = isWindows() ? ['.exe', '.cmd', ''] : [''];
|
|
19
|
+
const parts = (process.env.PATH || '').split(path.delimiter);
|
|
20
|
+
for (const dir of parts) {
|
|
21
|
+
for (const ext of exts) {
|
|
22
|
+
const candidate = path.join(dir, name + ext);
|
|
23
|
+
if (fileExists(candidate)) return candidate;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function coreBinary(): string {
|
|
30
|
+
// 1) Env override
|
|
31
|
+
const envPath = process.env.PACKAGEPURGE_CORE;
|
|
32
|
+
if (envPath && fileExists(envPath)) return envPath;
|
|
33
|
+
// 2) Local release/debug
|
|
34
|
+
const exe = isWindows() ? 'packagepurge_core.exe' : 'packagepurge-core';
|
|
35
|
+
const rel = path.join(process.cwd(), 'core', 'target', 'release', exe);
|
|
36
|
+
if (fileExists(rel)) return rel;
|
|
37
|
+
const dbg = path.join(process.cwd(), 'core', 'target', 'debug', exe);
|
|
38
|
+
if (fileExists(dbg)) return dbg;
|
|
39
|
+
// 3) PATH
|
|
40
|
+
const fromPath = resolveFromPATH(isWindows() ? 'packagepurge_core' : 'packagepurge-core');
|
|
41
|
+
if (fromPath) return fromPath;
|
|
42
|
+
throw new Error('packagepurge-core binary not found. Build with "npm run build:core" or set PACKAGEPURGE_CORE.');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function runCore(args: string[]): Promise<{ stdout: string; stderr: string; code: number }>
|
|
46
|
+
{
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
const bin = coreBinary();
|
|
49
|
+
const child = spawn(bin, args, { stdio: ['ignore', 'pipe', 'pipe'], env: process.env });
|
|
50
|
+
let out = '';
|
|
51
|
+
let err = '';
|
|
52
|
+
child.stdout.on('data', (d) => out += d.toString());
|
|
53
|
+
child.stderr.on('data', (d) => err += d.toString());
|
|
54
|
+
child.on('error', reject);
|
|
55
|
+
child.on('close', (code) => resolve({ stdout: out, stderr: err, code: code ?? 1 }));
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function output(data: string, format: 'json' | 'yaml') {
|
|
60
|
+
if (format === 'yaml') {
|
|
61
|
+
try {
|
|
62
|
+
const obj = JSON.parse(data);
|
|
63
|
+
console.log(YAML.stringify(obj));
|
|
64
|
+
return;
|
|
65
|
+
} catch {
|
|
66
|
+
// Fallback to raw
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
console.log(data);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const program = new Command();
|
|
73
|
+
program
|
|
74
|
+
.name('packagepurge')
|
|
75
|
+
.description('Intelligent package manager cache cleanup service with project-aware optimization')
|
|
76
|
+
.version('1.0.0')
|
|
77
|
+
.option('-q, --quiet', 'Minimal output', false)
|
|
78
|
+
.option('-v, --verbose', 'Verbose logging', false)
|
|
79
|
+
.option('-f, --format <format>', 'Output format: json|yaml', 'json');
|
|
80
|
+
|
|
81
|
+
program.hook('preAction', (_, actionCommand) => {
|
|
82
|
+
const opts = actionCommand.optsWithGlobals();
|
|
83
|
+
if (opts.verbose) logger.setLevel(0);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
program
|
|
87
|
+
.command('scan')
|
|
88
|
+
.description('Scan filesystem and output results')
|
|
89
|
+
.option('-p, --paths <paths...>', 'Paths to scan', [])
|
|
90
|
+
.action(async (opts, cmd) => {
|
|
91
|
+
const g = cmd.parent?.opts?.() || {};
|
|
92
|
+
const args = ['scan', ...(opts.paths?.length ? ['--paths', ...opts.paths] : [])];
|
|
93
|
+
const res = await runCore(args);
|
|
94
|
+
if (res.code !== 0) {
|
|
95
|
+
if (!g.quiet) logger.error(res.stderr || 'Scan failed');
|
|
96
|
+
process.exit(res.code);
|
|
97
|
+
}
|
|
98
|
+
await output(res.stdout, (g.format || 'json'));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
program
|
|
102
|
+
.command('analyze')
|
|
103
|
+
.description('Dry-run cleanup plan (no changes)')
|
|
104
|
+
.option('-p, --paths <paths...>', 'Paths to analyze', [])
|
|
105
|
+
.option('-d, --preserve-days <days>', 'Preserve days for recency', '90')
|
|
106
|
+
.action(async (opts, cmd) => {
|
|
107
|
+
const g = cmd.parent?.opts?.() || {};
|
|
108
|
+
const preserve = String(opts.preserveDays ?? opts['preserve-days'] ?? opts.d ?? 90);
|
|
109
|
+
const args = ['dry-run', '--preserve-days', preserve, ...(opts.paths?.length ? ['--paths', ...opts.paths] : [])];
|
|
110
|
+
const res = await runCore(args);
|
|
111
|
+
if (res.code !== 0) {
|
|
112
|
+
if (!g.quiet) logger.error(res.stderr || 'Analyze failed');
|
|
113
|
+
process.exit(res.code);
|
|
114
|
+
}
|
|
115
|
+
await output(res.stdout, (g.format || 'json'));
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
program
|
|
119
|
+
.command('clean')
|
|
120
|
+
.description('Quarantine targets (Move-and-Delete transaction). Defaults to dry-run via analyze.')
|
|
121
|
+
.option('-t, --targets <targets...>', 'Paths to quarantine (from analyze)')
|
|
122
|
+
.action(async (opts, cmd) => {
|
|
123
|
+
const g = cmd.parent?.opts?.() || {};
|
|
124
|
+
if (!opts.targets || !opts.targets.length) {
|
|
125
|
+
if (!g.quiet) logger.warn('No targets provided. Run analyze first to produce a plan.');
|
|
126
|
+
process.exit(2);
|
|
127
|
+
}
|
|
128
|
+
const res = await runCore(['quarantine', ...opts.targets]);
|
|
129
|
+
if (res.code !== 0) {
|
|
130
|
+
if (!g.quiet) logger.error(res.stderr || 'Clean failed');
|
|
131
|
+
process.exit(res.code);
|
|
132
|
+
}
|
|
133
|
+
await output(res.stdout, (g.format || 'json'));
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
program
|
|
137
|
+
.command('rollback')
|
|
138
|
+
.description('Rollback quarantine by id or latest')
|
|
139
|
+
.option('--id <id>', 'Quarantine record id')
|
|
140
|
+
.option('--latest', 'Rollback the most recent quarantine', false)
|
|
141
|
+
.action(async (opts, cmd) => {
|
|
142
|
+
const g = cmd.parent?.opts?.() || {};
|
|
143
|
+
const args = ['rollback'];
|
|
144
|
+
const finalArgs = opts.id ? args.concat(['--id', opts.id]) : (opts.latest ? args.concat(['--latest']) : args);
|
|
145
|
+
const res = await runCore(finalArgs);
|
|
146
|
+
if (res.code !== 0) {
|
|
147
|
+
if (!g.quiet) logger.error(res.stderr || 'Rollback failed');
|
|
148
|
+
process.exit(res.code);
|
|
149
|
+
}
|
|
150
|
+
await output(res.stdout, (g.format || 'json'));
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
program
|
|
154
|
+
.command('optimize')
|
|
155
|
+
.description('Optimize with ML/LRU prediction and symlinking (dry run)')
|
|
156
|
+
.option('-p, --paths <paths...>', 'Paths to optimize', [])
|
|
157
|
+
.option('-d, --preserve-days <days>', 'Days to preserve packages', '90')
|
|
158
|
+
.option('--enable-symlinking', 'Enable cross-project symlinking', false)
|
|
159
|
+
.option('--enable-ml', 'Enable ML-based predictions', false)
|
|
160
|
+
.option('--lru-max-packages <count>', 'Maximum packages in LRU cache', '1000')
|
|
161
|
+
.option('--lru-max-size-bytes <bytes>', 'Maximum size of LRU cache in bytes', '10000000000')
|
|
162
|
+
.action(async (opts, cmd) => {
|
|
163
|
+
const g = cmd.parent?.opts?.() || {};
|
|
164
|
+
const preserve = String(opts.preserveDays ?? opts['preserve-days'] ?? opts.d ?? 90);
|
|
165
|
+
const lruPackages = String(opts.lruMaxPackages ?? opts['lru-max-packages'] ?? 1000);
|
|
166
|
+
const lruSize = String(opts.lruMaxSizeBytes ?? opts['lru-max-size-bytes'] ?? 10000000000);
|
|
167
|
+
|
|
168
|
+
const args = [
|
|
169
|
+
'optimize',
|
|
170
|
+
'--preserve-days', preserve,
|
|
171
|
+
'--lru-max-packages', lruPackages,
|
|
172
|
+
'--lru-max-size-bytes', lruSize,
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
if (opts.enableSymlinking || opts['enable-symlinking']) {
|
|
176
|
+
args.push('--enable-symlinking');
|
|
177
|
+
}
|
|
178
|
+
if (opts.enableMl || opts['enable-ml']) {
|
|
179
|
+
args.push('--enable-ml');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (opts.paths?.length) {
|
|
183
|
+
args.push('--paths', ...opts.paths);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const res = await runCore(args);
|
|
187
|
+
if (res.code !== 0) {
|
|
188
|
+
if (!g.quiet) logger.error(res.stderr || 'Optimize failed');
|
|
189
|
+
process.exit(res.code);
|
|
190
|
+
}
|
|
191
|
+
await output(res.stdout, (g.format || 'json'));
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
program
|
|
195
|
+
.command('symlink')
|
|
196
|
+
.description('Execute symlinking for duplicate packages across projects')
|
|
197
|
+
.option('-p, --paths <paths...>', 'Paths to process', [])
|
|
198
|
+
.action(async (opts, cmd) => {
|
|
199
|
+
const g = cmd.parent?.opts?.() || {};
|
|
200
|
+
const args = ['symlink'];
|
|
201
|
+
if (opts.paths?.length) {
|
|
202
|
+
args.push('--paths', ...opts.paths);
|
|
203
|
+
}
|
|
204
|
+
const res = await runCore(args);
|
|
205
|
+
if (res.code !== 0) {
|
|
206
|
+
if (!g.quiet) logger.error(res.stderr || 'Symlink failed');
|
|
207
|
+
process.exit(res.code);
|
|
208
|
+
}
|
|
209
|
+
await output(res.stdout, (g.format || 'json'));
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript bindings for Rust core functionality
|
|
3
|
+
* Provides type-safe interfaces to the Rust binary
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { spawn } from 'child_process';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import { OptimizeResult, SymlinkResult } from '../types';
|
|
10
|
+
|
|
11
|
+
function isWindows(): boolean {
|
|
12
|
+
return process.platform === 'win32';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function fileExists(p: string): boolean {
|
|
16
|
+
try { return fs.existsSync(p); } catch { return false; }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function resolveFromPATH(name: string): string | null {
|
|
20
|
+
const exts = isWindows() ? ['.exe', '.cmd', ''] : [''];
|
|
21
|
+
const parts = (process.env.PATH || '').split(path.delimiter);
|
|
22
|
+
for (const dir of parts) {
|
|
23
|
+
for (const ext of exts) {
|
|
24
|
+
const candidate = path.join(dir, name + ext);
|
|
25
|
+
if (fileExists(candidate)) return candidate;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function coreBinary(): string {
|
|
32
|
+
// 1) Env override
|
|
33
|
+
const envPath = process.env.PACKAGEPURGE_CORE;
|
|
34
|
+
if (envPath && fileExists(envPath)) return envPath;
|
|
35
|
+
// 2) Local release/debug
|
|
36
|
+
const exe = isWindows() ? 'packagepurge_core.exe' : 'packagepurge-core';
|
|
37
|
+
const rel = path.join(process.cwd(), 'core', 'target', 'release', exe);
|
|
38
|
+
if (fileExists(rel)) return rel;
|
|
39
|
+
const dbg = path.join(process.cwd(), 'core', 'target', 'debug', exe);
|
|
40
|
+
if (fileExists(dbg)) return dbg;
|
|
41
|
+
// 3) PATH
|
|
42
|
+
const fromPath = resolveFromPATH(isWindows() ? 'packagepurge_core' : 'packagepurge-core');
|
|
43
|
+
if (fromPath) return fromPath;
|
|
44
|
+
throw new Error('packagepurge-core binary not found. Build with "npm run build:core" or set PACKAGEPURGE_CORE.');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function runCore(args: string[]): Promise<{ stdout: string; stderr: string; code: number }> {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const bin = coreBinary();
|
|
50
|
+
const child = spawn(bin, args, { stdio: ['ignore', 'pipe', 'pipe'], env: process.env });
|
|
51
|
+
let out = '';
|
|
52
|
+
let err = '';
|
|
53
|
+
child.stdout.on('data', (d) => out += d.toString());
|
|
54
|
+
child.stderr.on('data', (d) => err += d.toString());
|
|
55
|
+
child.on('error', reject);
|
|
56
|
+
child.on('close', (code) => resolve({ stdout: out, stderr: err, code: code ?? 1 }));
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface OptimizeOptions {
|
|
61
|
+
paths?: string[];
|
|
62
|
+
preserveDays?: number;
|
|
63
|
+
enableSymlinking?: boolean;
|
|
64
|
+
enableML?: boolean;
|
|
65
|
+
lruMaxPackages?: number;
|
|
66
|
+
lruMaxSizeBytes?: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface SymlinkOptions {
|
|
70
|
+
paths?: string[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Optimize packages with ML/LRU prediction and symlinking
|
|
75
|
+
*/
|
|
76
|
+
export async function optimize(options: OptimizeOptions = {}): Promise<OptimizeResult> {
|
|
77
|
+
const args = ['optimize'];
|
|
78
|
+
|
|
79
|
+
if (options.preserveDays !== undefined) {
|
|
80
|
+
args.push('--preserve-days', String(options.preserveDays));
|
|
81
|
+
}
|
|
82
|
+
if (options.enableSymlinking) {
|
|
83
|
+
args.push('--enable-symlinking');
|
|
84
|
+
}
|
|
85
|
+
if (options.enableML) {
|
|
86
|
+
args.push('--enable-ml');
|
|
87
|
+
}
|
|
88
|
+
if (options.lruMaxPackages !== undefined) {
|
|
89
|
+
args.push('--lru-max-packages', String(options.lruMaxPackages));
|
|
90
|
+
}
|
|
91
|
+
if (options.lruMaxSizeBytes !== undefined) {
|
|
92
|
+
args.push('--lru-max-size-bytes', String(options.lruMaxSizeBytes));
|
|
93
|
+
}
|
|
94
|
+
if (options.paths && options.paths.length > 0) {
|
|
95
|
+
args.push('--paths', ...options.paths);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const res = await runCore(args);
|
|
99
|
+
if (res.code !== 0) {
|
|
100
|
+
throw new Error(`Optimize failed: ${res.stderr || 'Unknown error'}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return JSON.parse(res.stdout) as OptimizeResult;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Execute symlinking for duplicate packages
|
|
108
|
+
*/
|
|
109
|
+
export async function executeSymlinking(options: SymlinkOptions = {}): Promise<SymlinkResult> {
|
|
110
|
+
const args = ['symlink'];
|
|
111
|
+
|
|
112
|
+
if (options.paths && options.paths.length > 0) {
|
|
113
|
+
args.push('--paths', ...options.paths);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const res = await runCore(args);
|
|
117
|
+
if (res.code !== 0) {
|
|
118
|
+
throw new Error(`Symlink failed: ${res.stderr || 'Unknown error'}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return JSON.parse(res.stdout) as SymlinkResult;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Scan filesystem for packages and projects
|
|
126
|
+
*/
|
|
127
|
+
export async function scan(paths: string[] = []): Promise<any> {
|
|
128
|
+
const args = ['scan'];
|
|
129
|
+
if (paths.length > 0) {
|
|
130
|
+
args.push('--paths', ...paths);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const res = await runCore(args);
|
|
134
|
+
if (res.code !== 0) {
|
|
135
|
+
throw new Error(`Scan failed: ${res.stderr || 'Unknown error'}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return JSON.parse(res.stdout);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Analyze (dry run) cleanup plan
|
|
143
|
+
*/
|
|
144
|
+
export async function analyze(paths: string[] = [], preserveDays: number = 90): Promise<any> {
|
|
145
|
+
const args = ['dry-run', '--preserve-days', String(preserveDays)];
|
|
146
|
+
if (paths.length > 0) {
|
|
147
|
+
args.push('--paths', ...paths);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const res = await runCore(args);
|
|
151
|
+
if (res.code !== 0) {
|
|
152
|
+
throw new Error(`Analyze failed: ${res.stderr || 'Unknown error'}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return JSON.parse(res.stdout);
|
|
156
|
+
}
|
|
157
|
+
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for package manager integrations
|
|
3
|
+
*/
|
|
4
|
+
import { PackageManager, PackageInfo, ProjectInfo } from '../types';
|
|
5
|
+
import * as fs from 'fs-extra';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
|
|
8
|
+
export abstract class BasePackageManager {
|
|
9
|
+
abstract readonly manager: PackageManager;
|
|
10
|
+
abstract readonly lockFileName: string;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get the cache directory path for this manager
|
|
14
|
+
*/
|
|
15
|
+
abstract getCachePath(): Promise<string>;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parse lock file to extract dependency information
|
|
19
|
+
*/
|
|
20
|
+
abstract parseLockFile(lockFilePath: string): Promise<Map<string, string>>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Scan for all packages in cache
|
|
24
|
+
*/
|
|
25
|
+
async scanPackages(): Promise<PackageInfo[]> {
|
|
26
|
+
const cachePath = await this.getCachePath();
|
|
27
|
+
if (!(await fs.pathExists(cachePath))) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const packages: PackageInfo[] = [];
|
|
32
|
+
const entries = await fs.readdir(cachePath);
|
|
33
|
+
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
const entryPath = path.join(cachePath, entry);
|
|
36
|
+
const stat = await fs.stat(entryPath);
|
|
37
|
+
|
|
38
|
+
if (stat.isDirectory()) {
|
|
39
|
+
const packageInfo = await this.analyzePackage(entryPath);
|
|
40
|
+
if (packageInfo) {
|
|
41
|
+
packages.push(packageInfo);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return packages;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Analyze a single package directory
|
|
51
|
+
*/
|
|
52
|
+
protected abstract analyzePackage(packagePath: string): Promise<PackageInfo | null>;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Find all projects using this manager
|
|
56
|
+
*/
|
|
57
|
+
async findProjects(rootDir: string = process.cwd()): Promise<ProjectInfo[]> {
|
|
58
|
+
const projects: ProjectInfo[] = [];
|
|
59
|
+
const lockFilePattern = this.lockFileName;
|
|
60
|
+
|
|
61
|
+
const searchDir = async (dir: string, depth: number = 0): Promise<void> => {
|
|
62
|
+
if (depth > 10) return; // Limit depth to prevent infinite recursion
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
66
|
+
const entryNames = entries.map(e => e.name);
|
|
67
|
+
const hasLockFile = entryNames.includes(lockFilePattern);
|
|
68
|
+
const hasPackageJson = entryNames.includes('package.json');
|
|
69
|
+
|
|
70
|
+
if (hasLockFile && hasPackageJson) {
|
|
71
|
+
const packageJsonPath = path.join(dir, 'package.json');
|
|
72
|
+
const lockFilePath = path.join(dir, lockFilePattern);
|
|
73
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
74
|
+
const dependencies = await this.parseLockFile(lockFilePath);
|
|
75
|
+
const stat = await fs.stat(packageJsonPath);
|
|
76
|
+
|
|
77
|
+
projects.push({
|
|
78
|
+
path: dir,
|
|
79
|
+
manager: this.manager,
|
|
80
|
+
dependencies,
|
|
81
|
+
lastModified: stat.mtime,
|
|
82
|
+
lockFilePath,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Recursively search subdirectories
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
|
|
89
|
+
await searchDir(path.join(dir, entry.name), depth + 1);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} catch (error) {
|
|
93
|
+
// Skip directories we can't read
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
await searchDir(rootDir);
|
|
98
|
+
return projects;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get package version from directory name or package.json
|
|
103
|
+
*/
|
|
104
|
+
protected async getPackageVersion(packagePath: string): Promise<string | null> {
|
|
105
|
+
const packageJsonPath = path.join(packagePath, 'package.json');
|
|
106
|
+
if (await fs.pathExists(packageJsonPath)) {
|
|
107
|
+
try {
|
|
108
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
109
|
+
return packageJson.version || null;
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package manager factory and exports
|
|
3
|
+
*/
|
|
4
|
+
import { BasePackageManager } from './base-manager';
|
|
5
|
+
import { NpmManager } from './npm-manager';
|
|
6
|
+
import { YarnManager } from './yarn-manager';
|
|
7
|
+
import { PnpmManager } from './pnpm-manager';
|
|
8
|
+
import { PackageManager } from '../types';
|
|
9
|
+
|
|
10
|
+
export { BasePackageManager, NpmManager, YarnManager, PnpmManager };
|
|
11
|
+
|
|
12
|
+
export function getManager(manager: PackageManager): BasePackageManager {
|
|
13
|
+
switch (manager) {
|
|
14
|
+
case PackageManager.NPM:
|
|
15
|
+
return new NpmManager();
|
|
16
|
+
case PackageManager.YARN:
|
|
17
|
+
return new YarnManager();
|
|
18
|
+
case PackageManager.PNPM:
|
|
19
|
+
return new PnpmManager();
|
|
20
|
+
default:
|
|
21
|
+
throw new Error(`Unsupported package manager: ${manager}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getAllManagers(): BasePackageManager[] {
|
|
26
|
+
return [
|
|
27
|
+
new NpmManager(),
|
|
28
|
+
new YarnManager(),
|
|
29
|
+
new PnpmManager(),
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
|