packagepurge 1.0.0 → 2.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/.agent/workflows/build.md +58 -0
- package/.github/workflows/release.yml +176 -0
- package/README.md +215 -49
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +122 -132
- package/dist/cli/index.js.map +1 -1
- package/dist/core/bindings.d.ts +11 -0
- package/dist/core/bindings.d.ts.map +1 -1
- package/dist/core/bindings.js +40 -94
- package/dist/core/bindings.js.map +1 -1
- package/dist/utils/core-utils.d.ts +31 -0
- package/dist/utils/core-utils.d.ts.map +1 -0
- package/dist/utils/core-utils.js +121 -0
- package/dist/utils/core-utils.js.map +1 -0
- package/dist/utils/formatter.d.ts +63 -0
- package/dist/utils/formatter.d.ts.map +1 -0
- package/dist/utils/formatter.js +295 -0
- package/dist/utils/formatter.js.map +1 -0
- package/package.json +3 -3
- package/core/src/arc_lfu.rs +0 -91
- package/core/src/cache.rs +0 -205
- package/core/src/lockfiles.rs +0 -112
- package/core/src/main.rs +0 -125
- package/core/src/ml.rs +0 -188
- package/core/src/optimization.rs +0 -314
- package/core/src/safety.rs +0 -103
- package/core/src/scanner.rs +0 -136
- package/core/src/symlink.rs +0 -223
- package/core/src/types.rs +0 -87
- package/core/src/usage_tracker.rs +0 -107
- package/src/cli/index.ts +0 -212
- package/src/core/bindings.ts +0 -157
- package/src/managers/base-manager.ts +0 -117
- package/src/managers/index.ts +0 -32
- package/src/managers/npm-manager.ts +0 -96
- package/src/managers/pnpm-manager.ts +0 -107
- package/src/managers/yarn-manager.ts +0 -112
- package/src/types/index.ts +0 -97
- package/src/utils/logger.ts +0 -50
- package/tsconfig.json +0 -22
package/src/core/bindings.ts
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
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
|
-
|
|
@@ -1,117 +0,0 @@
|
|
|
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
|
-
|
package/src/managers/index.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
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
|
-
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* npm package manager integration
|
|
3
|
-
*/
|
|
4
|
-
import { BasePackageManager } from './base-manager';
|
|
5
|
-
import { PackageManager, PackageInfo } from '../types';
|
|
6
|
-
import * as os from 'os';
|
|
7
|
-
import * as path from 'path';
|
|
8
|
-
import * as fs from 'fs-extra';
|
|
9
|
-
|
|
10
|
-
export class NpmManager extends BasePackageManager {
|
|
11
|
-
readonly manager = PackageManager.NPM;
|
|
12
|
-
readonly lockFileName = 'package-lock.json';
|
|
13
|
-
|
|
14
|
-
async getCachePath(): Promise<string> {
|
|
15
|
-
// npm cache path: ~/.npm on Unix, %AppData%/npm-cache on Windows
|
|
16
|
-
if (process.platform === 'win32') {
|
|
17
|
-
return path.join(os.homedir(), 'AppData', 'Roaming', 'npm-cache');
|
|
18
|
-
}
|
|
19
|
-
return path.join(os.homedir(), '.npm');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async parseLockFile(lockFilePath: string): Promise<Map<string, string>> {
|
|
23
|
-
const dependencies = new Map<string, string>();
|
|
24
|
-
|
|
25
|
-
try {
|
|
26
|
-
const lockFile = await fs.readJson(lockFilePath);
|
|
27
|
-
|
|
28
|
-
// Parse package-lock.json structure
|
|
29
|
-
function extractDependencies(obj: any, prefix: string = ''): void {
|
|
30
|
-
if (obj.dependencies) {
|
|
31
|
-
for (const [name, dep] of Object.entries(obj.dependencies as Record<string, any>)) {
|
|
32
|
-
const fullName = prefix ? `${prefix}/${name}` : name;
|
|
33
|
-
if (dep.version) {
|
|
34
|
-
dependencies.set(fullName, dep.version);
|
|
35
|
-
}
|
|
36
|
-
if (dep.dependencies) {
|
|
37
|
-
extractDependencies(dep, fullName);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
extractDependencies(lockFile);
|
|
44
|
-
} catch (error) {
|
|
45
|
-
// If lock file parsing fails, return empty map
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return dependencies;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
protected async analyzePackage(packagePath: string): Promise<PackageInfo | null> {
|
|
52
|
-
try {
|
|
53
|
-
const stat = await fs.stat(packagePath);
|
|
54
|
-
const packageJsonPath = path.join(packagePath, 'package.json');
|
|
55
|
-
|
|
56
|
-
if (!(await fs.pathExists(packageJsonPath))) {
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const packageJson = await fs.readJson(packageJsonPath);
|
|
61
|
-
const size = await this.calculateDirectorySize(packagePath);
|
|
62
|
-
|
|
63
|
-
return {
|
|
64
|
-
name: packageJson.name || path.basename(packagePath),
|
|
65
|
-
version: packageJson.version || 'unknown',
|
|
66
|
-
path: packagePath,
|
|
67
|
-
lastAccessed: stat.atime,
|
|
68
|
-
size,
|
|
69
|
-
manager: this.manager,
|
|
70
|
-
projectPaths: [],
|
|
71
|
-
};
|
|
72
|
-
} catch {
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
private async calculateDirectorySize(dirPath: string): Promise<number> {
|
|
78
|
-
let size = 0;
|
|
79
|
-
try {
|
|
80
|
-
const entries = await fs.readdir(dirPath);
|
|
81
|
-
for (const entry of entries) {
|
|
82
|
-
const entryPath = path.join(dirPath, entry);
|
|
83
|
-
const stat = await fs.stat(entryPath);
|
|
84
|
-
if (stat.isDirectory()) {
|
|
85
|
-
size += await this.calculateDirectorySize(entryPath);
|
|
86
|
-
} else {
|
|
87
|
-
size += stat.size;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
} catch {
|
|
91
|
-
// Ignore errors
|
|
92
|
-
}
|
|
93
|
-
return size;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pnpm package manager integration
|
|
3
|
-
*/
|
|
4
|
-
import { BasePackageManager } from './base-manager';
|
|
5
|
-
import { PackageManager, PackageInfo } from '../types';
|
|
6
|
-
import * as os from 'os';
|
|
7
|
-
import * as path from 'path';
|
|
8
|
-
import * as fs from 'fs-extra';
|
|
9
|
-
|
|
10
|
-
export class PnpmManager extends BasePackageManager {
|
|
11
|
-
readonly manager = PackageManager.PNPM;
|
|
12
|
-
readonly lockFileName = 'pnpm-lock.yaml';
|
|
13
|
-
|
|
14
|
-
async getCachePath(): Promise<string> {
|
|
15
|
-
// pnpm store path: ~/.pnpm-store on Unix, %LOCALAPPDATA%/pnpm/store on Windows
|
|
16
|
-
if (process.platform === 'win32') {
|
|
17
|
-
return path.join(os.homedir(), 'AppData', 'Local', 'pnpm', 'store');
|
|
18
|
-
}
|
|
19
|
-
return path.join(os.homedir(), '.pnpm-store');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async parseLockFile(lockFilePath: string): Promise<Map<string, string>> {
|
|
23
|
-
const dependencies = new Map<string, string>();
|
|
24
|
-
|
|
25
|
-
try {
|
|
26
|
-
const lockFileContent = await fs.readFile(lockFilePath, 'utf-8');
|
|
27
|
-
|
|
28
|
-
// Parse pnpm-lock.yaml format (YAML)
|
|
29
|
-
const lines = lockFileContent.split('\n');
|
|
30
|
-
let currentPackage: string | null = null;
|
|
31
|
-
let currentVersion: string | null = null;
|
|
32
|
-
|
|
33
|
-
for (const line of lines) {
|
|
34
|
-
const trimmed = line.trim();
|
|
35
|
-
|
|
36
|
-
// Package entry: "package-name:"
|
|
37
|
-
if (trimmed.endsWith(':') && !trimmed.startsWith(' ') && !trimmed.startsWith('#')) {
|
|
38
|
-
const match = trimmed.match(/^(.+?):$/);
|
|
39
|
-
if (match && !match[1].startsWith('lockfile')) {
|
|
40
|
-
currentPackage = match[1];
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Version field
|
|
45
|
-
if (trimmed.startsWith('version:') && currentPackage) {
|
|
46
|
-
const match = trimmed.match(/version:\s*(.+?)$/);
|
|
47
|
-
if (match) {
|
|
48
|
-
currentVersion = match[1].replace(/["']/g, '');
|
|
49
|
-
dependencies.set(currentPackage, currentVersion);
|
|
50
|
-
currentPackage = null;
|
|
51
|
-
currentVersion = null;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
} catch (error) {
|
|
56
|
-
// If lock file parsing fails, return empty map
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return dependencies;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
protected async analyzePackage(packagePath: string): Promise<PackageInfo | null> {
|
|
63
|
-
try {
|
|
64
|
-
const stat = await fs.stat(packagePath);
|
|
65
|
-
const packageJsonPath = path.join(packagePath, 'package.json');
|
|
66
|
-
|
|
67
|
-
if (!(await fs.pathExists(packageJsonPath))) {
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const packageJson = await fs.readJson(packageJsonPath);
|
|
72
|
-
const size = await this.calculateDirectorySize(packagePath);
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
name: packageJson.name || path.basename(packagePath),
|
|
76
|
-
version: packageJson.version || 'unknown',
|
|
77
|
-
path: packagePath,
|
|
78
|
-
lastAccessed: stat.atime,
|
|
79
|
-
size,
|
|
80
|
-
manager: this.manager,
|
|
81
|
-
projectPaths: [],
|
|
82
|
-
};
|
|
83
|
-
} catch {
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
private async calculateDirectorySize(dirPath: string): Promise<number> {
|
|
89
|
-
let size = 0;
|
|
90
|
-
try {
|
|
91
|
-
const entries = await fs.readdir(dirPath);
|
|
92
|
-
for (const entry of entries) {
|
|
93
|
-
const entryPath = path.join(dirPath, entry);
|
|
94
|
-
const stat = await fs.stat(entryPath);
|
|
95
|
-
if (stat.isDirectory()) {
|
|
96
|
-
size += await this.calculateDirectorySize(entryPath);
|
|
97
|
-
} else {
|
|
98
|
-
size += stat.size;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
} catch {
|
|
102
|
-
// Ignore errors
|
|
103
|
-
}
|
|
104
|
-
return size;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* yarn package manager integration
|
|
3
|
-
*/
|
|
4
|
-
import { BasePackageManager } from './base-manager';
|
|
5
|
-
import { PackageManager, PackageInfo } from '../types';
|
|
6
|
-
import * as os from 'os';
|
|
7
|
-
import * as path from 'path';
|
|
8
|
-
import * as fs from 'fs-extra';
|
|
9
|
-
|
|
10
|
-
export class YarnManager extends BasePackageManager {
|
|
11
|
-
readonly manager = PackageManager.YARN;
|
|
12
|
-
readonly lockFileName = 'yarn.lock';
|
|
13
|
-
|
|
14
|
-
async getCachePath(): Promise<string> {
|
|
15
|
-
// yarn cache path: ~/.yarn/cache on Unix, %LOCALAPPDATA%/Yarn/Cache on Windows
|
|
16
|
-
if (process.platform === 'win32') {
|
|
17
|
-
return path.join(os.homedir(), 'AppData', 'Local', 'Yarn', 'Cache');
|
|
18
|
-
}
|
|
19
|
-
return path.join(os.homedir(), '.yarn', 'cache');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async parseLockFile(lockFilePath: string): Promise<Map<string, string>> {
|
|
23
|
-
const dependencies = new Map<string, string>();
|
|
24
|
-
|
|
25
|
-
try {
|
|
26
|
-
const lockFileContent = await fs.readFile(lockFilePath, 'utf-8');
|
|
27
|
-
|
|
28
|
-
// Parse yarn.lock format (YAML-like format)
|
|
29
|
-
const lines = lockFileContent.split('\n');
|
|
30
|
-
let currentPackage: string | null = null;
|
|
31
|
-
let currentVersion: string | null = null;
|
|
32
|
-
|
|
33
|
-
for (const line of lines) {
|
|
34
|
-
const trimmed = line.trim();
|
|
35
|
-
|
|
36
|
-
// Package declaration: "package-name@version:"
|
|
37
|
-
if (trimmed.endsWith(':') && !trimmed.startsWith(' ')) {
|
|
38
|
-
const match = trimmed.match(/^"?(.+?)@(.+?)"?:$/);
|
|
39
|
-
if (match) {
|
|
40
|
-
currentPackage = match[1];
|
|
41
|
-
currentVersion = match[2];
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Version field
|
|
46
|
-
if (trimmed.startsWith('version') && currentPackage) {
|
|
47
|
-
const match = trimmed.match(/version\s+"?(.+?)"?$/);
|
|
48
|
-
if (match) {
|
|
49
|
-
currentVersion = match[1];
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Store dependency
|
|
54
|
-
if (currentPackage && currentVersion && trimmed.startsWith('resolved')) {
|
|
55
|
-
dependencies.set(currentPackage, currentVersion);
|
|
56
|
-
currentPackage = null;
|
|
57
|
-
currentVersion = null;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
} catch (error) {
|
|
61
|
-
// If lock file parsing fails, return empty map
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return dependencies;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
protected async analyzePackage(packagePath: string): Promise<PackageInfo | null> {
|
|
68
|
-
try {
|
|
69
|
-
const stat = await fs.stat(packagePath);
|
|
70
|
-
const packageJsonPath = path.join(packagePath, 'package.json');
|
|
71
|
-
|
|
72
|
-
if (!(await fs.pathExists(packageJsonPath))) {
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const packageJson = await fs.readJson(packageJsonPath);
|
|
77
|
-
const size = await this.calculateDirectorySize(packagePath);
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
name: packageJson.name || path.basename(packagePath),
|
|
81
|
-
version: packageJson.version || 'unknown',
|
|
82
|
-
path: packagePath,
|
|
83
|
-
lastAccessed: stat.atime,
|
|
84
|
-
size,
|
|
85
|
-
manager: this.manager,
|
|
86
|
-
projectPaths: [],
|
|
87
|
-
};
|
|
88
|
-
} catch {
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
private async calculateDirectorySize(dirPath: string): Promise<number> {
|
|
94
|
-
let size = 0;
|
|
95
|
-
try {
|
|
96
|
-
const entries = await fs.readdir(dirPath);
|
|
97
|
-
for (const entry of entries) {
|
|
98
|
-
const entryPath = path.join(dirPath, entry);
|
|
99
|
-
const stat = await fs.stat(entryPath);
|
|
100
|
-
if (stat.isDirectory()) {
|
|
101
|
-
size += await this.calculateDirectorySize(entryPath);
|
|
102
|
-
} else {
|
|
103
|
-
size += stat.size;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
} catch {
|
|
107
|
-
// Ignore errors
|
|
108
|
-
}
|
|
109
|
-
return size;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|