freeform-modeling-mcp 1.0.26
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/README.md +61 -0
- package/index.cjs +21 -0
- package/package.json +48 -0
- package/src/cli.ts +47 -0
- package/src/commands/start.ts +88 -0
- package/src/commands/status.ts +81 -0
- package/src/dev.ts +24 -0
- package/src/lib/fileproxy/index.ts +505 -0
- package/src/lib/fileproxy/proxy-server.js +347 -0
- package/src/lib/gltf2obj/compress.js +33 -0
- package/src/lib/gltf2obj/engine/core/core.js +34 -0
- package/src/lib/gltf2obj/engine/core/eventnotifier.js +39 -0
- package/src/lib/gltf2obj/engine/core/taskrunner.js +88 -0
- package/src/lib/gltf2obj/engine/export/exporter.js +38 -0
- package/src/lib/gltf2obj/engine/export/exporterbase.js +86 -0
- package/src/lib/gltf2obj/engine/export/exportermodel.js +117 -0
- package/src/lib/gltf2obj/engine/export/exporterobj.js +147 -0
- package/src/lib/gltf2obj/engine/geometry/box3d.js +60 -0
- package/src/lib/gltf2obj/engine/geometry/coord2d.js +35 -0
- package/src/lib/gltf2obj/engine/geometry/coord3d.js +126 -0
- package/src/lib/gltf2obj/engine/geometry/coord4d.js +15 -0
- package/src/lib/gltf2obj/engine/geometry/geometry.js +56 -0
- package/src/lib/gltf2obj/engine/geometry/matrix.js +440 -0
- package/src/lib/gltf2obj/engine/geometry/octree.js +160 -0
- package/src/lib/gltf2obj/engine/geometry/quaternion.js +83 -0
- package/src/lib/gltf2obj/engine/geometry/transformation.js +63 -0
- package/src/lib/gltf2obj/engine/geometry/tween.js +31 -0
- package/src/lib/gltf2obj/engine/import/importer.js +270 -0
- package/src/lib/gltf2obj/engine/import/importerbase.js +115 -0
- package/src/lib/gltf2obj/engine/import/importerfiles.js +139 -0
- package/src/lib/gltf2obj/engine/import/importergltf.js +1045 -0
- package/src/lib/gltf2obj/engine/import/importerutils.js +102 -0
- package/src/lib/gltf2obj/engine/io/binaryreader.js +93 -0
- package/src/lib/gltf2obj/engine/io/binarywriter.js +92 -0
- package/src/lib/gltf2obj/engine/io/bufferutils.js +85 -0
- package/src/lib/gltf2obj/engine/io/externallibs.js +42 -0
- package/src/lib/gltf2obj/engine/io/fileutils.js +120 -0
- package/src/lib/gltf2obj/engine/io/textwriter.js +41 -0
- package/src/lib/gltf2obj/engine/main.js +19 -0
- package/src/lib/gltf2obj/engine/model/color.js +130 -0
- package/src/lib/gltf2obj/engine/model/generator.js +433 -0
- package/src/lib/gltf2obj/engine/model/material.js +243 -0
- package/src/lib/gltf2obj/engine/model/mesh.js +173 -0
- package/src/lib/gltf2obj/engine/model/meshbuffer.js +233 -0
- package/src/lib/gltf2obj/engine/model/meshinstance.js +128 -0
- package/src/lib/gltf2obj/engine/model/meshutils.js +64 -0
- package/src/lib/gltf2obj/engine/model/model.js +195 -0
- package/src/lib/gltf2obj/engine/model/modelfinalization.js +377 -0
- package/src/lib/gltf2obj/engine/model/modelutils.js +117 -0
- package/src/lib/gltf2obj/engine/model/node.js +178 -0
- package/src/lib/gltf2obj/engine/model/object.js +90 -0
- package/src/lib/gltf2obj/engine/model/property.js +86 -0
- package/src/lib/gltf2obj/engine/model/quantities.js +46 -0
- package/src/lib/gltf2obj/engine/model/topology.js +139 -0
- package/src/lib/gltf2obj/engine/model/triangle.js +99 -0
- package/src/lib/gltf2obj/engine/threejs/threemodelloader.js +85 -0
- package/src/lib/gltf2obj/handler.js +63 -0
- package/src/lib/gltf2obj/index.js +47 -0
- package/src/lib/logger.ts +304 -0
- package/src/lib/print.ts +37 -0
- package/src/lib/utils.ts +34 -0
- package/src/lib/wsbridge/bridge.ts +639 -0
- package/src/lib/wsbridge/lock.ts +258 -0
- package/src/llmClient.ts +245 -0
- package/src/prompt.ts +187 -0
- package/src/server.ts +125 -0
- package/src/tools/assets-tool.ts +314 -0
- package/src/tools/basic-tools.ts +531 -0
- package/src/tools/index.ts +2 -0
- package/src/tools/material-tools.ts +88 -0
- package/src/tools/modeling/auxiliary-curve-tool.ts +44 -0
- package/src/tools/modeling/find-face.ts +38 -0
- package/src/tools/modeling/sweep-tool.ts +60 -0
- package/src/tools/modeling/utils.ts +181 -0
- package/src/tools/modeling-tools.ts +135 -0
- package/src/tools/screenshot-tools.ts +107 -0
- package/src/tools/tools-info.ts +107 -0
- package/src/tools/tripo3d-tools.ts +496 -0
- package/src/types/index.ts +25 -0
- package/src/types/tripo3d.ts +38 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as util from 'util';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const __filenameNew = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirnameNew = path.dirname(__filenameNew)
|
|
8
|
+
|
|
9
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
10
|
+
|
|
11
|
+
// 日志级别枚举
|
|
12
|
+
export enum LogLevel {
|
|
13
|
+
DEBUG = 0,
|
|
14
|
+
INFO = 1,
|
|
15
|
+
WARN = 2,
|
|
16
|
+
ERROR = 3,
|
|
17
|
+
NONE = 100 // 用于完全禁用日志
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class Logger {
|
|
21
|
+
private logFile: fs.WriteStream | null = null;
|
|
22
|
+
private logLevel: LogLevel = LogLevel.INFO;
|
|
23
|
+
private logDir: string;
|
|
24
|
+
private baseFilename: string;
|
|
25
|
+
private currentLogPath: string = '';
|
|
26
|
+
private rotationSize: number; // 单位: 字节
|
|
27
|
+
private maxLogFiles: number;
|
|
28
|
+
private isFileReady: boolean = false;
|
|
29
|
+
private pendingMessages: string[] = [];
|
|
30
|
+
|
|
31
|
+
constructor(options: {
|
|
32
|
+
logDir?: string;
|
|
33
|
+
baseFilename?: string;
|
|
34
|
+
logLevel?: LogLevel;
|
|
35
|
+
rotationSize?: number; // 单位: MB
|
|
36
|
+
maxLogFiles?: number;
|
|
37
|
+
} = {}) {
|
|
38
|
+
// 设置默认值和用户配置
|
|
39
|
+
this.logDir = options.logDir || path.join(path.resolve(__dirnameNew, "../../"), 'logs');
|
|
40
|
+
this.baseFilename = options.baseFilename || 'koomaster-mcp';
|
|
41
|
+
this.logLevel = options.logLevel !== undefined ? options.logLevel : LogLevel.INFO;
|
|
42
|
+
this.rotationSize = (options.rotationSize || 5) * 1024 * 1024; // 默认 5MB
|
|
43
|
+
this.maxLogFiles = options.maxLogFiles || 5;
|
|
44
|
+
|
|
45
|
+
// 确保日志目录存在
|
|
46
|
+
this.ensureLogDirectory();
|
|
47
|
+
|
|
48
|
+
// 创建日志文件
|
|
49
|
+
this.createLogFile();
|
|
50
|
+
|
|
51
|
+
// 捕获未处理的异常和拒绝,确保它们被记录
|
|
52
|
+
process.on('uncaughtException', (error) => {
|
|
53
|
+
this.error(`Uncaught Exception: ${error.stack || error.toString()}`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
process.on('unhandledRejection', (reason) => {
|
|
58
|
+
this.error(`Unhandled Rejection: ${reason instanceof Error ? reason.stack : util.inspect(reason)}`);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 确保日志目录存在
|
|
63
|
+
private ensureLogDirectory(): void {
|
|
64
|
+
try {
|
|
65
|
+
if (!fs.existsSync(this.logDir)) {
|
|
66
|
+
fs.mkdirSync(this.logDir, { recursive: true });
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
// console.error(`Failed to create log directory: ${error instanceof Error ? error.message : String(error)}`);
|
|
70
|
+
// 不抛出错误,继续使用控制台作为回退
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 创建新的日志文件
|
|
75
|
+
private createLogFile(): void {
|
|
76
|
+
try {
|
|
77
|
+
// 关闭现有的日志文件(如果有)
|
|
78
|
+
if (this.logFile) {
|
|
79
|
+
this.logFile.end();
|
|
80
|
+
this.logFile = null;
|
|
81
|
+
this.isFileReady = false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 创建带时间戳的日志文件名
|
|
85
|
+
const filename = `${this.baseFilename}-${new Date().toISOString().replace(/[:.]/g, '-')}.log`;
|
|
86
|
+
this.currentLogPath = path.join(this.logDir, filename);
|
|
87
|
+
|
|
88
|
+
// 先创建空文件确保它存在
|
|
89
|
+
fs.writeFileSync(this.currentLogPath, '');
|
|
90
|
+
|
|
91
|
+
// 创建文件流
|
|
92
|
+
this.logFile = fs.createWriteStream(this.currentLogPath, { flags: 'a' });
|
|
93
|
+
|
|
94
|
+
// 设置打开事件处理程序
|
|
95
|
+
this.logFile.on('open', () => {
|
|
96
|
+
this.isFileReady = true;
|
|
97
|
+
// console.log(`Log file created: ${this.currentLogPath}`);
|
|
98
|
+
|
|
99
|
+
// 写入内部启动消息
|
|
100
|
+
this._writeToFile(`[${new Date().toISOString()}] [INFO] Log file created: ${this.currentLogPath}\n`);
|
|
101
|
+
|
|
102
|
+
// 处理待处理的消息
|
|
103
|
+
if (this.pendingMessages.length > 0) {
|
|
104
|
+
this.pendingMessages.forEach(msg => this._writeToFile(msg));
|
|
105
|
+
this.pendingMessages = [];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 执行日志轮转 - 删除超过最大数量的旧日志
|
|
109
|
+
this.rotateLogFiles();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// 设置错误事件处理程序
|
|
113
|
+
this.logFile.on('error', (error) => {
|
|
114
|
+
// console.error(`Error with log file: ${error.message}`);
|
|
115
|
+
this.isFileReady = false;
|
|
116
|
+
this.logFile = null;
|
|
117
|
+
});
|
|
118
|
+
} catch (error) {
|
|
119
|
+
// console.error(`Failed to create log file: ${error instanceof Error ? error.message : String(error)}`);
|
|
120
|
+
// 出错时回退到控制台日志
|
|
121
|
+
this.isFileReady = false;
|
|
122
|
+
this.logFile = null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 确保文件存在的辅助方法
|
|
127
|
+
private ensureFileExists(filePath: string): boolean {
|
|
128
|
+
try {
|
|
129
|
+
if (!fs.existsSync(filePath)) {
|
|
130
|
+
fs.writeFileSync(filePath, '');
|
|
131
|
+
}
|
|
132
|
+
return true;
|
|
133
|
+
} catch (error) {
|
|
134
|
+
// console.error(`Failed to ensure file exists: ${error instanceof Error ? error.message : String(error)}`);
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 日志轮转 - 删除旧的日志文件
|
|
140
|
+
private rotateLogFiles(): void {
|
|
141
|
+
try {
|
|
142
|
+
// 读取日志目录中的所有文件
|
|
143
|
+
const files = fs.readdirSync(this.logDir);
|
|
144
|
+
|
|
145
|
+
// 过滤出匹配基本文件名的日志文件
|
|
146
|
+
const logFiles = files
|
|
147
|
+
.filter(file => file.startsWith(this.baseFilename) && file.endsWith('.log'))
|
|
148
|
+
.map(file => ({
|
|
149
|
+
name: file,
|
|
150
|
+
path: path.join(this.logDir, file),
|
|
151
|
+
time: fs.statSync(path.join(this.logDir, file)).mtime.getTime()
|
|
152
|
+
}))
|
|
153
|
+
.sort((a, b) => b.time - a.time); // 按修改时间降序排序
|
|
154
|
+
|
|
155
|
+
// 仅保留最近的 N 个文件
|
|
156
|
+
if (logFiles.length > this.maxLogFiles) {
|
|
157
|
+
for (let i = this.maxLogFiles; i < logFiles.length; i++) {
|
|
158
|
+
try {
|
|
159
|
+
fs.unlinkSync(logFiles[i].path);
|
|
160
|
+
this._writeToFile(`[${new Date().toISOString()}] [DEBUG] Deleted old log file: ${logFiles[i].name}\n`);
|
|
161
|
+
} catch (e) {
|
|
162
|
+
// console.warn(`Failed to delete old log file: ${logFiles[i].name}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} catch (error) {
|
|
167
|
+
// console.warn(`Error during log rotation: ${error instanceof Error ? error.message : String(error)}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 检查日志文件大小,必要时创建新文件
|
|
172
|
+
private checkLogFileSize(): void {
|
|
173
|
+
if (!this.logFile || !this.isFileReady || !this.currentLogPath) return;
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
if (fs.existsSync(this.currentLogPath)) {
|
|
177
|
+
const stats = fs.statSync(this.currentLogPath);
|
|
178
|
+
if (stats.size >= this.rotationSize) {
|
|
179
|
+
this._writeToFile(`[${new Date().toISOString()}] [INFO] Log file size (${stats.size} bytes) exceeded rotation threshold, creating new log file\n`);
|
|
180
|
+
this.createLogFile();
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
// 文件不存在,重新创建
|
|
184
|
+
// console.warn(`Log file ${this.currentLogPath} does not exist, recreating`);
|
|
185
|
+
this.createLogFile();
|
|
186
|
+
}
|
|
187
|
+
} catch (error) {
|
|
188
|
+
// console.warn(`Error checking log file size: ${error instanceof Error ? error.message : String(error)}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 直接写入文件的内部方法
|
|
193
|
+
private _writeToFile(message: string): void {
|
|
194
|
+
if (!this.logFile || !this.isFileReady) return;
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
this.logFile.write(message);
|
|
198
|
+
} catch (error) {
|
|
199
|
+
// console.error(`Failed to write to log file: ${error instanceof Error ? error.message : String(error)}`);
|
|
200
|
+
this.isFileReady = false;
|
|
201
|
+
|
|
202
|
+
// 尝试重新创建日志文件
|
|
203
|
+
setTimeout(() => this.createLogFile(), 1000);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 写入日志入口
|
|
208
|
+
private log(level: string, message: string): void {
|
|
209
|
+
const timestamp = new Date().toISOString();
|
|
210
|
+
const logEntry = `[${timestamp}] [${level}] ${message}\n`;
|
|
211
|
+
|
|
212
|
+
// 如果日志文件就绪,写入日志文件
|
|
213
|
+
if (this.isFileReady && this.logFile) {
|
|
214
|
+
this._writeToFile(logEntry);
|
|
215
|
+
this.checkLogFileSize();
|
|
216
|
+
} else if (this.logFile) {
|
|
217
|
+
// 文件流存在但未就绪,将消息加入队列
|
|
218
|
+
this.pendingMessages.push(logEntry);
|
|
219
|
+
} else {
|
|
220
|
+
// 回退到控制台日志
|
|
221
|
+
// console.log(logEntry.trim());
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 设置日志级别
|
|
226
|
+
public setLogLevel(level: LogLevel): void {
|
|
227
|
+
this.logLevel = level;
|
|
228
|
+
this.info(`Log level set to: ${LogLevel[level]}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 获取当前日志级别
|
|
232
|
+
public getLogLevel(): LogLevel {
|
|
233
|
+
return this.logLevel;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// 获取当前日志文件路径
|
|
237
|
+
public getCurrentLogPath(): string {
|
|
238
|
+
return this.currentLogPath;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 调试日志
|
|
242
|
+
public debug(message: string): void {
|
|
243
|
+
if (this.logLevel <= LogLevel.DEBUG) {
|
|
244
|
+
this.log('DEBUG', message);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 信息日志
|
|
249
|
+
public info(message: string, ...args: any[]): void {
|
|
250
|
+
if (this.logLevel <= LogLevel.INFO) {
|
|
251
|
+
this.log('INFO', message + args.join(" "));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 警告日志
|
|
256
|
+
public warn(message: string, ...args: any[]): void {
|
|
257
|
+
if (this.logLevel <= LogLevel.WARN) {
|
|
258
|
+
this.log('WARN', message + args.join(" "));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// 错误日志
|
|
263
|
+
public error(message: string, error?: any): void {
|
|
264
|
+
if (this.logLevel <= LogLevel.ERROR) {
|
|
265
|
+
this.log('ERROR', message + (error ? (": " + error.message + "\n" + error.stack) : ""));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// 关闭日志
|
|
270
|
+
public close(): void {
|
|
271
|
+
if (this.logFile) {
|
|
272
|
+
this.info('Closing log file');
|
|
273
|
+
this.logFile.end();
|
|
274
|
+
this.logFile = null;
|
|
275
|
+
this.isFileReady = false;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 强制将所有待写入的日志刷新到磁盘
|
|
280
|
+
public flush(): Promise<void> {
|
|
281
|
+
return new Promise<void>((resolve) => {
|
|
282
|
+
if (!this.logFile || !this.isFileReady) {
|
|
283
|
+
resolve();
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
this.logFile.once('drain', () => {
|
|
288
|
+
resolve();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// 也设置一个超时,以防 'drain' 事件未触发
|
|
292
|
+
setTimeout(() => {
|
|
293
|
+
resolve();
|
|
294
|
+
}, 1000);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// 创建单例记录器实例
|
|
300
|
+
const logger = new Logger();
|
|
301
|
+
|
|
302
|
+
// 导出单例和类型
|
|
303
|
+
export { logger };
|
|
304
|
+
export default logger;
|
package/src/lib/print.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import figures from 'figures';
|
|
3
|
+
|
|
4
|
+
export function printServerInfo(
|
|
5
|
+
title: string,
|
|
6
|
+
items: { label: string; value: string | number }[],
|
|
7
|
+
instruction?: { icon?: string; text: string; value?: string | number }
|
|
8
|
+
) {
|
|
9
|
+
// 清空一行
|
|
10
|
+
const emptyLine = () => console.log('');
|
|
11
|
+
|
|
12
|
+
emptyLine();
|
|
13
|
+
console.log(chalk.bold.greenBright(`${figures.star} ${title}`));
|
|
14
|
+
emptyLine();
|
|
15
|
+
|
|
16
|
+
// 打印每个项目
|
|
17
|
+
items.forEach(item => {
|
|
18
|
+
console.log(` ${chalk.cyan(item.label)} ${chalk.bold.white(item.value)}`);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
emptyLine();
|
|
22
|
+
|
|
23
|
+
// 打印说明(如果有)
|
|
24
|
+
if (instruction) {
|
|
25
|
+
const icon = instruction.icon || figures.pointer;
|
|
26
|
+
const value = instruction.value !== undefined ? String(instruction.value) : '';
|
|
27
|
+
const text = instruction.text.replace('VALUE', value);
|
|
28
|
+
|
|
29
|
+
console.log(chalk.yellow(` ${figures.info} ${chalk.bold('Connection Instructions:')}`));
|
|
30
|
+
console.log(` ${icon} ${chalk.greenBright(text)}`);
|
|
31
|
+
emptyLine();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 添加分隔线
|
|
35
|
+
console.log(chalk.gray('─'.repeat(process.stdout.columns || 80)));
|
|
36
|
+
emptyLine();
|
|
37
|
+
}
|
package/src/lib/utils.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import net from 'net';
|
|
2
|
+
|
|
3
|
+
// 检查端口是否可用
|
|
4
|
+
export async function isPortAvailable(port: number): Promise<boolean> {
|
|
5
|
+
return new Promise((resolve) => {
|
|
6
|
+
const server = net.createServer()
|
|
7
|
+
.once('error', (err: any) => {
|
|
8
|
+
// 如果端口已被占用,会抛出 EADDRINUSE 错误
|
|
9
|
+
if (err.code === 'EADDRINUSE') {
|
|
10
|
+
resolve(false);
|
|
11
|
+
} else {
|
|
12
|
+
resolve(false); // 其他错误也视为不可用
|
|
13
|
+
}
|
|
14
|
+
})
|
|
15
|
+
.once('listening', () => {
|
|
16
|
+
// 端口可用,关闭测试服务器
|
|
17
|
+
server.close();
|
|
18
|
+
resolve(true);
|
|
19
|
+
})
|
|
20
|
+
.listen(port);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 查找可用端口
|
|
25
|
+
export async function findAvailablePort(startPort: number, maxTries: number = 100): Promise<number> {
|
|
26
|
+
let port = startPort;
|
|
27
|
+
for (let i = 0; i < maxTries; i++) {
|
|
28
|
+
if (await isPortAvailable(port)) {
|
|
29
|
+
return port;
|
|
30
|
+
}
|
|
31
|
+
port++;
|
|
32
|
+
}
|
|
33
|
+
throw new Error(`Could not find available port in range ${startPort} to ${startPort + maxTries - 1}`);
|
|
34
|
+
}
|