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.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +125 -0
  3. package/core/Cargo.lock +1093 -0
  4. package/core/Cargo.toml +22 -0
  5. package/core/src/arc_lfu.rs +91 -0
  6. package/core/src/cache.rs +205 -0
  7. package/core/src/lockfiles.rs +112 -0
  8. package/core/src/main.rs +125 -0
  9. package/core/src/ml.rs +188 -0
  10. package/core/src/optimization.rs +314 -0
  11. package/core/src/safety.rs +103 -0
  12. package/core/src/scanner.rs +136 -0
  13. package/core/src/symlink.rs +223 -0
  14. package/core/src/types.rs +87 -0
  15. package/core/src/usage_tracker.rs +107 -0
  16. package/dist/cli/index.d.ts +2 -0
  17. package/dist/cli/index.d.ts.map +1 -0
  18. package/dist/cli/index.js +249 -0
  19. package/dist/cli/index.js.map +1 -0
  20. package/dist/core/bindings.d.ts +33 -0
  21. package/dist/core/bindings.d.ts.map +1 -0
  22. package/dist/core/bindings.js +172 -0
  23. package/dist/core/bindings.js.map +1 -0
  24. package/dist/managers/base-manager.d.ts +33 -0
  25. package/dist/managers/base-manager.d.ts.map +1 -0
  26. package/dist/managers/base-manager.js +122 -0
  27. package/dist/managers/base-manager.js.map +1 -0
  28. package/dist/managers/index.d.ts +12 -0
  29. package/dist/managers/index.d.ts.map +1 -0
  30. package/dist/managers/index.js +37 -0
  31. package/dist/managers/index.js.map +1 -0
  32. package/dist/managers/npm-manager.d.ts +14 -0
  33. package/dist/managers/npm-manager.d.ts.map +1 -0
  34. package/dist/managers/npm-manager.js +128 -0
  35. package/dist/managers/npm-manager.js.map +1 -0
  36. package/dist/managers/pnpm-manager.d.ts +14 -0
  37. package/dist/managers/pnpm-manager.d.ts.map +1 -0
  38. package/dist/managers/pnpm-manager.js +137 -0
  39. package/dist/managers/pnpm-manager.js.map +1 -0
  40. package/dist/managers/yarn-manager.d.ts +14 -0
  41. package/dist/managers/yarn-manager.d.ts.map +1 -0
  42. package/dist/managers/yarn-manager.js +141 -0
  43. package/dist/managers/yarn-manager.js.map +1 -0
  44. package/dist/types/index.d.ts +85 -0
  45. package/dist/types/index.d.ts.map +1 -0
  46. package/dist/types/index.js +13 -0
  47. package/dist/types/index.js.map +1 -0
  48. package/dist/utils/logger.d.ts +18 -0
  49. package/dist/utils/logger.d.ts.map +1 -0
  50. package/dist/utils/logger.js +50 -0
  51. package/dist/utils/logger.js.map +1 -0
  52. package/package.json +64 -0
  53. package/src/cli/index.ts +212 -0
  54. package/src/core/bindings.ts +157 -0
  55. package/src/managers/base-manager.ts +117 -0
  56. package/src/managers/index.ts +32 -0
  57. package/src/managers/npm-manager.ts +96 -0
  58. package/src/managers/pnpm-manager.ts +107 -0
  59. package/src/managers/yarn-manager.ts +112 -0
  60. package/src/types/index.ts +97 -0
  61. package/src/utils/logger.ts +50 -0
  62. 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
+
@@ -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
+