@dimzxzzx07/mc-headless 2.2.1 → 2.2.3

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 (35) hide show
  1. package/README.md +658 -765
  2. package/dist/core/JavaChecker.d.ts +8 -2
  3. package/dist/core/JavaChecker.d.ts.map +1 -1
  4. package/dist/core/JavaChecker.js +219 -104
  5. package/dist/core/JavaChecker.js.map +1 -1
  6. package/dist/core/MinecraftServer.d.ts +17 -32
  7. package/dist/core/MinecraftServer.d.ts.map +1 -1
  8. package/dist/core/MinecraftServer.js +449 -186
  9. package/dist/core/MinecraftServer.js.map +1 -1
  10. package/dist/index.d.ts +19 -18
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +40 -18
  13. package/dist/index.js.map +1 -1
  14. package/dist/platforms/GeyserBridge.d.ts +21 -3
  15. package/dist/platforms/GeyserBridge.d.ts.map +1 -1
  16. package/dist/platforms/GeyserBridge.js +160 -60
  17. package/dist/platforms/GeyserBridge.js.map +1 -1
  18. package/dist/platforms/PluginManager.d.ts +16 -0
  19. package/dist/platforms/PluginManager.d.ts.map +1 -0
  20. package/dist/platforms/PluginManager.js +208 -0
  21. package/dist/platforms/PluginManager.js.map +1 -0
  22. package/dist/platforms/ViaVersion.d.ts +1 -7
  23. package/dist/platforms/ViaVersion.d.ts.map +1 -1
  24. package/dist/platforms/ViaVersion.js +23 -87
  25. package/dist/platforms/ViaVersion.js.map +1 -1
  26. package/dist/types/index.d.ts +189 -0
  27. package/dist/types/index.d.ts.map +1 -1
  28. package/package.json +4 -2
  29. package/src/core/JavaChecker.ts +224 -108
  30. package/src/core/MinecraftServer.ts +540 -254
  31. package/src/index.ts +19 -19
  32. package/src/platforms/GeyserBridge.ts +196 -80
  33. package/src/platforms/PluginManager.ts +206 -0
  34. package/src/platforms/ViaVersion.ts +26 -107
  35. package/src/types/index.ts +206 -0
package/src/index.ts CHANGED
@@ -4,25 +4,25 @@ import { MinecraftServer } from './core/MinecraftServer';
4
4
  import { Logger, LogLevel } from './utils/Logger';
5
5
  import { SystemDetector } from './utils/SystemDetector';
6
6
  import { JavaChecker } from './core/JavaChecker';
7
-
8
- export * from './core/MinecraftServer';
9
- export * from './core/ConfigHandler';
10
- export * from './core/JavaChecker';
11
- export * from './core/ServerManager';
12
- export * from './platforms/JavaServer';
13
- export * from './platforms/BedrockServer';
14
- export * from './platforms/GeyserBridge';
15
- export * from './platforms/ViaVersion';
16
- export * from './platforms/SkinRestorer';
17
- export * from './engines/Downloader';
18
- export * from './engines/PaperEngine';
19
- export * from './engines/VanillaEngine';
20
- export * from './engines/ForgeEngine';
21
- export * from './engines/FabricEngine';
22
- export * from './utils/Logger';
23
- export * from './utils/FileUtils';
24
- export * from './utils/SystemDetector';
25
- export * from './utils/PropertiesParser';
7
+ export { MinecraftServer } from './core/MinecraftServer';
8
+ export { ConfigHandler } from './core/ConfigHandler';
9
+ export { JavaChecker, JavaInfo } from './core/JavaChecker';
10
+ export { ServerManager } from './core/ServerManager';
11
+ export { JavaServer } from './platforms/JavaServer';
12
+ export { BedrockServer } from './platforms/BedrockServer';
13
+ export { GeyserBridge, GeyserConfig } from './platforms/GeyserBridge';
14
+ export { ViaVersionManager } from './platforms/ViaVersion';
15
+ export { SkinRestorerManager } from './platforms/SkinRestorer';
16
+ export { PluginManager } from './platforms/PluginManager';
17
+ export { Downloader } from './engines/Downloader';
18
+ export { PaperEngine } from './engines/PaperEngine';
19
+ export { VanillaEngine } from './engines/VanillaEngine';
20
+ export { ForgeEngine } from './engines/ForgeEngine';
21
+ export { FabricEngine } from './engines/FabricEngine';
22
+ export { Logger, LogLevel } from './utils/Logger';
23
+ export { FileUtils } from './utils/FileUtils';
24
+ export { SystemDetector } from './utils/SystemDetector';
25
+ export { PropertiesParser } from './utils/PropertiesParser';
26
26
  export * from './types';
27
27
 
28
28
  export default MinecraftServer;
@@ -1,111 +1,227 @@
1
1
  import { MinecraftConfig } from '../types';
2
- import { Downloader } from '../engines/Downloader';
3
2
  import { FileUtils } from '../utils/FileUtils';
4
3
  import { Logger } from '../utils/Logger';
5
4
  import * as path from 'path';
6
- import { spawn } from 'child_process';
5
+ import * as fs from 'fs-extra';
6
+ import * as yaml from 'js-yaml';
7
+
8
+ export interface GeyserConfig {
9
+ bedrock: {
10
+ port: number;
11
+ address: string;
12
+ };
13
+ remote: {
14
+ address: string;
15
+ port: number;
16
+ authType: string;
17
+ };
18
+ floodgateKeyFile: string;
19
+ passthroughMotd: boolean;
20
+ passthroughPlayerCounts: boolean;
21
+ passthroughProtocolName: boolean;
22
+ }
7
23
 
8
24
  export class GeyserBridge {
9
25
  private logger = Logger.getInstance();
10
- private process: any = null;
11
26
 
12
- public async setup(config: MinecraftConfig): Promise<void> {
27
+ public async setup(config: MinecraftConfig, isPterodactyl: boolean = false): Promise<void> {
13
28
  const pluginsDir = config.folders.plugins;
14
-
15
29
  await FileUtils.ensureDir(pluginsDir);
16
30
 
17
- const geyserJar = path.join(pluginsDir, 'geyser.jar');
18
- const floodgateJar = path.join(pluginsDir, 'floodgate.jar');
31
+ this.logger.info('Setting up Geyser & Floodgate...');
32
+
33
+ await this.cleanupRemappedFolders(pluginsDir);
34
+ await this.downloadGeyser(pluginsDir);
35
+ await this.downloadFloodgate(pluginsDir);
36
+
37
+ if (isPterodactyl) {
38
+ await this.configureGeyserForPterodactyl(config, pluginsDir);
39
+ } else {
40
+ await this.configureGeyser(config, pluginsDir);
41
+ }
19
42
 
20
- if (!await FileUtils.fileExists(geyserJar)) {
21
- this.logger.info('Downloading Geyser...');
22
-
23
- const geyserInfo = await Downloader.getGeyserInfo();
24
-
25
- geyserInfo.fileName = 'geyser.jar';
26
-
27
- const downloadedPath = await Downloader.downloadFile(geyserInfo, pluginsDir);
28
-
29
- if (downloadedPath && downloadedPath !== geyserJar) {
30
- if (await FileUtils.fileExists(downloadedPath)) {
31
- await FileUtils.moveFile(downloadedPath, geyserJar);
43
+ this.logger.success('Geyser & Floodgate installed');
44
+ }
45
+
46
+ private async cleanupRemappedFolders(pluginsDir: string): Promise<void> {
47
+ try {
48
+ const files = await fs.readdir(pluginsDir);
49
+ for (const file of files) {
50
+ if (file.includes('.paper-remapped')) {
51
+ const remappedPath = path.join(pluginsDir, file);
52
+ const stat = await fs.stat(remappedPath);
53
+ if (stat.isDirectory()) {
54
+ this.logger.warning(`Removing corrupt remapped folder: ${file}`);
55
+ await fs.remove(remappedPath);
56
+ }
32
57
  }
33
58
  }
34
-
35
- this.logger.success('Geyser downloaded to plugins folder');
59
+ } catch (error) {
36
60
  }
61
+ }
37
62
 
38
- if (!await FileUtils.fileExists(floodgateJar)) {
39
- this.logger.info('Downloading Floodgate...');
40
-
41
- const floodgateInfo = await Downloader.getFloodgateInfo();
42
-
43
- floodgateInfo.fileName = 'floodgate.jar';
44
-
45
- const downloadedPath = await Downloader.downloadFile(floodgateInfo, pluginsDir);
46
-
47
- if (downloadedPath && downloadedPath !== floodgateJar) {
48
- if (await FileUtils.fileExists(downloadedPath)) {
49
- await FileUtils.moveFile(downloadedPath, floodgateJar);
50
- }
63
+ private async downloadGeyser(pluginsDir: string): Promise<void> {
64
+ const geyserJar = path.join(pluginsDir, 'Geyser-Spigot.jar');
65
+
66
+ const possibleNames = ['geyser.jar', 'Geyser.jar', 'Geyser-Spigot.jar'];
67
+ for (const name of possibleNames) {
68
+ const oldPath = path.join(pluginsDir, name);
69
+ if (await FileUtils.fileExists(oldPath)) {
70
+ await fs.remove(oldPath);
51
71
  }
52
-
53
- this.logger.success('Floodgate downloaded to plugins folder');
54
72
  }
55
73
 
56
- this.logger.success('Geyser & Floodgate installed');
74
+ this.logger.info('Downloading Geyser...');
75
+
76
+ try {
77
+ const response = await require('axios')({
78
+ method: 'GET',
79
+ url: 'https://download.geysermc.org/v2/projects/geyser/versions/latest/builds/latest/downloads/spigot',
80
+ responseType: 'stream',
81
+ timeout: 60000,
82
+ headers: {
83
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
84
+ }
85
+ });
86
+
87
+ const writer = fs.createWriteStream(geyserJar);
88
+ response.data.pipe(writer);
89
+
90
+ await new Promise<void>((resolve, reject) => {
91
+ writer.on('finish', () => resolve());
92
+ writer.on('error', (err) => reject(err));
93
+ });
94
+
95
+ const stats = await fs.stat(geyserJar);
96
+ if (stats.size < 100000) {
97
+ throw new Error(`Downloaded file too small: ${stats.size} bytes`);
98
+ }
99
+
100
+ this.logger.success(`Geyser downloaded (${(stats.size / 1024 / 1024).toFixed(2)} MB)`);
101
+
102
+ } catch (error) {
103
+ const errorMessage = error instanceof Error ? error.message : String(error);
104
+ this.logger.error(`Failed to download Geyser: ${errorMessage}`);
105
+ throw error;
106
+ }
57
107
  }
58
108
 
59
- public async startStandalone(config: MinecraftConfig, serverDir: string): Promise<void> {
60
- const geyserDir = path.join(serverDir, 'geyser-standalone');
61
- await FileUtils.ensureDir(geyserDir);
109
+ private async downloadFloodgate(pluginsDir: string): Promise<void> {
110
+ const floodgateJar = path.join(pluginsDir, 'floodgate.jar');
62
111
 
63
- const geyserJar = path.join(geyserDir, 'geyser.jar');
64
-
65
- if (!await FileUtils.fileExists(geyserJar)) {
66
- this.logger.info('Downloading Geyser standalone...');
67
-
68
- const geyserInfo = await Downloader.getGeyserInfo();
69
- geyserInfo.fileName = 'geyser.jar';
70
-
71
- const downloadedPath = await Downloader.downloadFile(geyserInfo, geyserDir);
72
-
73
- if (downloadedPath && downloadedPath !== geyserJar) {
74
- if (await FileUtils.fileExists(downloadedPath)) {
75
- await FileUtils.moveFile(downloadedPath, geyserJar);
112
+ if (await FileUtils.fileExists(floodgateJar)) {
113
+ await fs.remove(floodgateJar);
114
+ }
115
+
116
+ this.logger.info('Downloading Floodgate...');
117
+
118
+ try {
119
+ const response = await require('axios')({
120
+ method: 'GET',
121
+ url: 'https://download.geysermc.org/v2/projects/floodgate/versions/latest/builds/latest/downloads/spigot',
122
+ responseType: 'stream',
123
+ timeout: 60000,
124
+ headers: {
125
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
76
126
  }
127
+ });
128
+
129
+ const writer = fs.createWriteStream(floodgateJar);
130
+ response.data.pipe(writer);
131
+
132
+ await new Promise<void>((resolve, reject) => {
133
+ writer.on('finish', () => resolve());
134
+ writer.on('error', (err) => reject(err));
135
+ });
136
+
137
+ const stats = await fs.stat(floodgateJar);
138
+ if (stats.size < 100000) {
139
+ throw new Error(`Downloaded file too small: ${stats.size} bytes`);
77
140
  }
78
-
79
- this.logger.success('Geyser standalone downloaded');
141
+
142
+ this.logger.success(`Floodgate downloaded (${(stats.size / 1024 / 1024).toFixed(2)} MB)`);
143
+
144
+ } catch (error) {
145
+ const errorMessage = error instanceof Error ? error.message : String(error);
146
+ this.logger.error(`Failed to download Floodgate: ${errorMessage}`);
147
+ throw error;
80
148
  }
149
+ }
81
150
 
82
- const javaArgs = [
83
- `-Xms${config.memory.init}`,
84
- `-Xmx${config.memory.max}`,
85
- '-jar',
86
- geyserJar
87
- ];
88
-
89
- this.logger.info('Starting Geyser standalone...');
90
- this.process = spawn('java', javaArgs, {
91
- cwd: geyserDir,
92
- stdio: 'pipe'
93
- });
94
-
95
- this.process.stdout.on('data', (data: Buffer) => {
96
- process.stdout.write(`[Geyser] ${data.toString()}`);
97
- });
98
-
99
- this.process.stderr.on('data', (data: Buffer) => {
100
- process.stderr.write(`[Geyser Error] ${data.toString()}`);
101
- });
151
+ private async configureGeyser(config: MinecraftConfig, pluginsDir: string): Promise<void> {
152
+ const geyserConfigPath = path.join(pluginsDir, 'Geyser-Spigot', 'config.yml');
153
+ await fs.ensureDir(path.dirname(geyserConfigPath));
154
+
155
+ const geyserConfig: GeyserConfig = {
156
+ bedrock: {
157
+ port: config.network.bedrockPort || 19132,
158
+ address: config.network.ip
159
+ },
160
+ remote: {
161
+ address: '127.0.0.1',
162
+ port: config.network.port,
163
+ authType: 'floodgate'
164
+ },
165
+ floodgateKeyFile: 'floodgate/key.pem',
166
+ passthroughMotd: true,
167
+ passthroughPlayerCounts: true,
168
+ passthroughProtocolName: true
169
+ };
170
+
171
+ await fs.writeFile(geyserConfigPath, yaml.dump(geyserConfig));
172
+ this.logger.debug('Geyser configured for standalone mode');
102
173
  }
103
174
 
104
- public stop(): void {
105
- if (this.process) {
106
- this.process.kill();
107
- this.process = null;
108
- this.logger.info('Geyser stopped');
175
+ private async configureGeyserForPterodactyl(config: MinecraftConfig, pluginsDir: string): Promise<void> {
176
+ const geyserConfigPath = path.join(pluginsDir, 'Geyser-Spigot', 'config.yml');
177
+ await fs.ensureDir(path.dirname(geyserConfigPath));
178
+
179
+ const pterodactylPort = parseInt(process.env.SERVER_PORT || config.network.port.toString());
180
+ const hasSeparateBedrockPort = !!(process.env.BEDROCK_PORT || config.network.bedrockPort);
181
+
182
+ let geyserConfig: GeyserConfig;
183
+
184
+ if (hasSeparateBedrockPort) {
185
+ geyserConfig = {
186
+ bedrock: {
187
+ port: parseInt(process.env.BEDROCK_PORT || config.network.bedrockPort?.toString() || '19132'),
188
+ address: '0.0.0.0'
189
+ },
190
+ remote: {
191
+ address: '127.0.0.1',
192
+ port: pterodactylPort,
193
+ authType: 'floodgate'
194
+ },
195
+ floodgateKeyFile: 'floodgate/key.pem',
196
+ passthroughMotd: true,
197
+ passthroughPlayerCounts: true,
198
+ passthroughProtocolName: true
199
+ };
200
+ this.logger.info(`Geyser configured with separate Bedrock port: ${geyserConfig.bedrock.port}`);
201
+ } else {
202
+ geyserConfig = {
203
+ bedrock: {
204
+ port: pterodactylPort,
205
+ address: '0.0.0.0'
206
+ },
207
+ remote: {
208
+ address: '127.0.0.1',
209
+ port: pterodactylPort,
210
+ authType: 'floodgate'
211
+ },
212
+ floodgateKeyFile: 'floodgate/key.pem',
213
+ passthroughMotd: true,
214
+ passthroughPlayerCounts: true,
215
+ passthroughProtocolName: true
216
+ };
217
+ this.logger.info(`Geyser configured for port sharing on ${pterodactylPort} (TCP+UDP required)`);
218
+ this.logger.warning('Make sure Pterodactyl panel has UDP enabled for this port');
109
219
  }
220
+
221
+ await fs.writeFile(geyserConfigPath, yaml.dump(geyserConfig));
222
+ this.logger.debug('Geyser configured for Pterodactyl mode');
223
+ }
224
+
225
+ public stop(): void {
110
226
  }
111
227
  }
@@ -0,0 +1,206 @@
1
+ import { FileUtils } from '../utils/FileUtils';
2
+ import { Logger } from '../utils/Logger';
3
+ import * as path from 'path';
4
+ import axios from 'axios';
5
+ import * as fs from 'fs-extra';
6
+ import * as stream from 'stream';
7
+ import * as util from 'util';
8
+
9
+ const pipeline = util.promisify(stream.pipeline);
10
+ const finished = util.promisify(stream.finished);
11
+
12
+ export class PluginManager {
13
+ private logger = Logger.getInstance();
14
+
15
+ public async installPlugin(name: string, url: string, pluginsDir: string): Promise<void> {
16
+ const pluginJar = path.join(pluginsDir, `${name}.jar`);
17
+
18
+ if (await FileUtils.fileExists(pluginJar)) {
19
+ this.logger.info(`${name} already exists, skipping download`);
20
+ return;
21
+ }
22
+
23
+ this.logger.info(`Downloading ${name}...`);
24
+
25
+ try {
26
+ const response = await axios({
27
+ method: 'GET',
28
+ url: url,
29
+ responseType: 'stream',
30
+ timeout: 60000,
31
+ headers: {
32
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
33
+ 'Accept': 'application/octet-stream'
34
+ },
35
+ maxRedirects: 5
36
+ });
37
+
38
+ if (response.status !== 200) {
39
+ throw new Error(`HTTP ${response.status}`);
40
+ }
41
+
42
+ const writer = fs.createWriteStream(pluginJar);
43
+ await pipeline(response.data, writer);
44
+ await finished(writer);
45
+
46
+ const stats = await fs.stat(pluginJar);
47
+ if (stats.size < 100000) {
48
+ await fs.remove(pluginJar);
49
+ throw new Error(`File too small: ${stats.size} bytes`);
50
+ }
51
+
52
+ this.logger.success(`${name} downloaded (${(stats.size / 1024 / 1024).toFixed(2)} MB)`);
53
+
54
+ } catch (error) {
55
+ const errorMessage = error instanceof Error ? error.message : String(error);
56
+ this.logger.error(`Failed to download ${name}: ${errorMessage}`);
57
+ }
58
+ }
59
+
60
+ public async installMultiple(plugins: Array<{name: string, url: string}>, pluginsDir: string): Promise<void> {
61
+ for (let i = 0; i < plugins.length; i++) {
62
+ const plugin = plugins[i];
63
+ await this.installPlugin(plugin.name, plugin.url, pluginsDir);
64
+
65
+ if (i < plugins.length - 1) {
66
+ await new Promise(resolve => setTimeout(resolve, 1000));
67
+ }
68
+ }
69
+ }
70
+
71
+ public async installFromGithub(repo: string, assetName: string, pluginsDir: string): Promise<void> {
72
+ const apiUrl = `https://api.github.com/repos/${repo}/releases/latest`;
73
+
74
+ try {
75
+ const response = await axios.get(apiUrl, {
76
+ headers: {
77
+ 'User-Agent': 'Mozilla/5.0'
78
+ }
79
+ });
80
+
81
+ const asset = response.data.assets.find((a: any) => a.name.includes(assetName));
82
+ if (!asset) {
83
+ throw new Error(`Asset ${assetName} not found in latest release`);
84
+ }
85
+
86
+ await this.installPlugin(
87
+ asset.name.replace('.jar', ''),
88
+ asset.browser_download_url,
89
+ pluginsDir
90
+ );
91
+
92
+ } catch (error) {
93
+ const errorMessage = error instanceof Error ? error.message : String(error);
94
+ this.logger.error(`Failed to install from GitHub ${repo}: ${errorMessage}`);
95
+ }
96
+ }
97
+
98
+ public async updatePlugin(name: string, url: string, pluginsDir: string): Promise<void> {
99
+ const pluginJar = path.join(pluginsDir, `${name}.jar`);
100
+
101
+ if (await FileUtils.fileExists(pluginJar)) {
102
+ await fs.remove(pluginJar);
103
+ }
104
+
105
+ await this.installPlugin(name, url, pluginsDir);
106
+ }
107
+
108
+ public async removePlugin(name: string, pluginsDir: string): Promise<void> {
109
+ const pluginJar = path.join(pluginsDir, `${name}.jar`);
110
+
111
+ if (await FileUtils.fileExists(pluginJar)) {
112
+ await fs.remove(pluginJar);
113
+ this.logger.info(`${name} removed`);
114
+ }
115
+ }
116
+
117
+ public async listPlugins(pluginsDir: string): Promise<string[]> {
118
+ if (!await FileUtils.fileExists(pluginsDir)) {
119
+ return [];
120
+ }
121
+
122
+ const files = await fs.readdir(pluginsDir);
123
+ return files.filter(f => f.endsWith('.jar')).map(f => f.replace('.jar', ''));
124
+ }
125
+
126
+ public async getPluginInfo(pluginName: string, pluginsDir: string): Promise<any> {
127
+ const pluginJar = path.join(pluginsDir, `${pluginName}.jar`);
128
+
129
+ if (!await FileUtils.fileExists(pluginJar)) {
130
+ return null;
131
+ }
132
+
133
+ const stats = await fs.stat(pluginJar);
134
+ return {
135
+ name: pluginName,
136
+ size: stats.size,
137
+ modified: stats.mtime,
138
+ path: pluginJar
139
+ };
140
+ }
141
+
142
+ public async verifyPlugin(pluginName: string, pluginsDir: string): Promise<boolean> {
143
+ const pluginJar = path.join(pluginsDir, `${pluginName}.jar`);
144
+
145
+ if (!await FileUtils.fileExists(pluginJar)) {
146
+ return false;
147
+ }
148
+
149
+ const stats = await fs.stat(pluginJar);
150
+ if (stats.size < 100000) {
151
+ return false;
152
+ }
153
+
154
+ try {
155
+ const buffer = await fs.readFile(pluginJar);
156
+ if (buffer[0] !== 0x50 || buffer[1] !== 0x4B) {
157
+ return false;
158
+ }
159
+ } catch {
160
+ return false;
161
+ }
162
+
163
+ return true;
164
+ }
165
+
166
+ public async cleanupCorruptPlugins(pluginsDir: string): Promise<number> {
167
+ if (!await FileUtils.fileExists(pluginsDir)) {
168
+ return 0;
169
+ }
170
+
171
+ let cleaned = 0;
172
+ const files = await fs.readdir(pluginsDir);
173
+
174
+ for (const file of files) {
175
+ if (!file.endsWith('.jar')) continue;
176
+
177
+ const filePath = path.join(pluginsDir, file);
178
+ try {
179
+ const stats = await fs.stat(filePath);
180
+ if (stats.size < 100000) {
181
+ await fs.remove(filePath);
182
+ this.logger.warning(`Removed corrupt plugin: ${file}`);
183
+ cleaned++;
184
+ continue;
185
+ }
186
+
187
+ const buffer = await fs.readFile(filePath);
188
+ if (buffer[0] !== 0x50 || buffer[1] !== 0x4B) {
189
+ await fs.remove(filePath);
190
+ this.logger.warning(`Removed invalid ZIP: ${file}`);
191
+ cleaned++;
192
+ }
193
+ } catch {
194
+ await fs.remove(filePath);
195
+ this.logger.warning(`Removed inaccessible plugin: ${file}`);
196
+ cleaned++;
197
+ }
198
+ }
199
+
200
+ if (cleaned > 0) {
201
+ this.logger.info(`Cleaned up ${cleaned} corrupt plugins`);
202
+ }
203
+
204
+ return cleaned;
205
+ }
206
+ }