@wanggangqi/workspace-cli 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/README.md +410 -0
- package/bin/cli.js +177 -0
- package/config.json +41 -0
- package/lib/workspace.js +1591 -0
- package/nginx-config/3620_zhongtai.conf +73 -0
- package/nginx-config/reload-nginx.ps1 +106 -0
- package/nginx-config/start-nginx.ps1 +163 -0
- package/nginx-config/status-nginx.ps1 +96 -0
- package/nginx-config/stop-nginx.ps1 +127 -0
- package/package.json +41 -0
- package/prompts/prompt.md +264 -0
package/lib/workspace.js
ADDED
|
@@ -0,0 +1,1591 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { execSync } = require("child_process");
|
|
4
|
+
const chalk = require("chalk");
|
|
5
|
+
const inquirer = require("inquirer");
|
|
6
|
+
|
|
7
|
+
class WorkspaceManager {
|
|
8
|
+
constructor(targetDir = null) {
|
|
9
|
+
if (targetDir) {
|
|
10
|
+
this.baseDir = path.resolve(targetDir);
|
|
11
|
+
} else {
|
|
12
|
+
// 自动查找工作空间根目录
|
|
13
|
+
this.baseDir = this.findWorkspaceRoot(process.cwd()) || process.cwd();
|
|
14
|
+
}
|
|
15
|
+
this.workspaceDir = path.join(this.baseDir, ".workspace");
|
|
16
|
+
this.configPath = path.join(this.workspaceDir, "config.json");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 向上查找工作空间根目录
|
|
21
|
+
*/
|
|
22
|
+
findWorkspaceRoot(startDir) {
|
|
23
|
+
let currentDir = startDir;
|
|
24
|
+
while (currentDir !== path.dirname(currentDir)) {
|
|
25
|
+
const workspaceDir = path.join(currentDir, ".workspace");
|
|
26
|
+
if (
|
|
27
|
+
fs.existsSync(workspaceDir) &&
|
|
28
|
+
fs.existsSync(path.join(workspaceDir, "config.json"))
|
|
29
|
+
) {
|
|
30
|
+
return currentDir;
|
|
31
|
+
}
|
|
32
|
+
currentDir = path.dirname(currentDir);
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 更新基础目录(用于指定目录初始化时)
|
|
39
|
+
*/
|
|
40
|
+
setBaseDir(targetDir) {
|
|
41
|
+
this.baseDir = path.resolve(targetDir);
|
|
42
|
+
this.workspaceDir = path.join(this.baseDir, ".workspace");
|
|
43
|
+
this.configPath = path.join(this.workspaceDir, "config.json");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 获取默认配置(从包自带的 config.json)
|
|
48
|
+
*/
|
|
49
|
+
getDefaultConfig() {
|
|
50
|
+
try {
|
|
51
|
+
// 尝试从多个位置查找默认配置
|
|
52
|
+
const possiblePaths = [
|
|
53
|
+
path.join(__dirname, "..", "config.json"),
|
|
54
|
+
path.join(process.cwd(), "config.json"),
|
|
55
|
+
path.join(__dirname, "..", "..", "config.json"),
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
for (const configPath of possiblePaths) {
|
|
59
|
+
if (fs.existsSync(configPath)) {
|
|
60
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
61
|
+
const config = JSON.parse(content);
|
|
62
|
+
// 保留平台组的仓库信息,清空 other 分组的 repos
|
|
63
|
+
for (const [groupName, group] of Object.entries(config.groups)) {
|
|
64
|
+
if (groupName !== "平台组") {
|
|
65
|
+
group.repos = [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return config;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
// 忽略错误,返回空配置
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
version: "1.0",
|
|
77
|
+
groups: {},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 确保配置文件存在
|
|
83
|
+
*/
|
|
84
|
+
ensureConfig(useDefault = false) {
|
|
85
|
+
if (!fs.existsSync(this.workspaceDir)) {
|
|
86
|
+
fs.mkdirSync(this.workspaceDir, { recursive: true });
|
|
87
|
+
}
|
|
88
|
+
if (!fs.existsSync(this.configPath)) {
|
|
89
|
+
const defaultConfig = useDefault
|
|
90
|
+
? this.getDefaultConfig()
|
|
91
|
+
: {
|
|
92
|
+
version: "1.0",
|
|
93
|
+
groups: {},
|
|
94
|
+
};
|
|
95
|
+
this.saveConfig(defaultConfig);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 读取配置文件
|
|
101
|
+
*/
|
|
102
|
+
loadConfig() {
|
|
103
|
+
this.ensureConfig();
|
|
104
|
+
const content = fs.readFileSync(this.configPath, "utf-8");
|
|
105
|
+
return JSON.parse(content);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 保存配置文件
|
|
110
|
+
*/
|
|
111
|
+
saveConfig(config) {
|
|
112
|
+
fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 获取支持的 AI 编辑器列表
|
|
117
|
+
*/
|
|
118
|
+
getSupportedEditors() {
|
|
119
|
+
return ["claude-code", "trae", "opencode", "iflow"];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 获取 AI 编辑器对应的 prompt 文件路径
|
|
124
|
+
* @param {string} aiEditor - AI 编辑器类型
|
|
125
|
+
* @returns {object|null} - 包含 targetPath 和 fileName 的对象
|
|
126
|
+
*/
|
|
127
|
+
getPromptFileInfo(aiEditor) {
|
|
128
|
+
const supportedEditors = this.getSupportedEditors();
|
|
129
|
+
|
|
130
|
+
if (!supportedEditors.includes(aiEditor)) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 定义各编辑器对应的文件路径
|
|
135
|
+
const editorFileMap = {
|
|
136
|
+
"claude-code": { fileName: "CLAUDE.md", dir: "" },
|
|
137
|
+
trae: { fileName: "prompt.md", dir: ".trae" },
|
|
138
|
+
opencode: { fileName: "prompt.md", dir: ".opencode" },
|
|
139
|
+
iflow: { fileName: "prompt.md", dir: ".iflow" },
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const config = editorFileMap[aiEditor];
|
|
143
|
+
if (!config) return null;
|
|
144
|
+
|
|
145
|
+
const targetDir = config.dir
|
|
146
|
+
? path.join(this.baseDir, config.dir)
|
|
147
|
+
: this.baseDir;
|
|
148
|
+
const targetPath = path.join(targetDir, config.fileName);
|
|
149
|
+
|
|
150
|
+
return { targetPath, targetDir, fileName: config.fileName };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 生成 AI 编辑器的 prompt 文件
|
|
155
|
+
* @param {string} aiEditor - AI 编辑器类型
|
|
156
|
+
*/
|
|
157
|
+
generatePromptFile(aiEditor) {
|
|
158
|
+
const supportedEditors = this.getSupportedEditors();
|
|
159
|
+
|
|
160
|
+
if (!supportedEditors.includes(aiEditor)) {
|
|
161
|
+
console.log(chalk.yellow(`⚠ 不支持的编辑器类型: ${aiEditor}`));
|
|
162
|
+
console.log(chalk.gray(` 支持的类型: ${supportedEditors.join(", ")}`));
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 查找通用 prompt.md 模板文件
|
|
167
|
+
const templatePaths = [
|
|
168
|
+
path.join(__dirname, "..", "prompts", "prompt.md"),
|
|
169
|
+
path.join(process.cwd(), "prompt.md"),
|
|
170
|
+
path.join(__dirname, "..", "..", "prompt.md"),
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
let templateContent = null;
|
|
174
|
+
let foundTemplatePath = null;
|
|
175
|
+
for (const templatePath of templatePaths) {
|
|
176
|
+
if (fs.existsSync(templatePath)) {
|
|
177
|
+
templateContent = fs.readFileSync(templatePath, "utf-8");
|
|
178
|
+
foundTemplatePath = templatePath;
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!templateContent) {
|
|
184
|
+
console.log(chalk.yellow(`⚠ 未找到 prompt.md 模板文件`));
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 获取目标文件信息
|
|
189
|
+
const fileInfo = this.getPromptFileInfo(aiEditor);
|
|
190
|
+
if (!fileInfo) {
|
|
191
|
+
console.log(chalk.yellow(`⚠ 无法获取 ${aiEditor} 的文件配置`));
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const { targetPath, targetDir } = fileInfo;
|
|
196
|
+
|
|
197
|
+
// 确保目录存在
|
|
198
|
+
if (!fs.existsSync(targetDir)) {
|
|
199
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 写入文件
|
|
203
|
+
fs.writeFileSync(targetPath, templateContent, "utf-8");
|
|
204
|
+
console.log(chalk.green(`✓ 生成 ${aiEditor} prompt 文件: ${targetPath}`));
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* 初始化工作空间
|
|
210
|
+
* @param {string|null} directory - 可选的目录名称,如果指定则创建该目录并初始化
|
|
211
|
+
* @param {string|null} aiEditor - 可选的 AI 编辑器类型
|
|
212
|
+
*/
|
|
213
|
+
async init(directory = null, aiEditor = null) {
|
|
214
|
+
// 如果指定了目录,更新路径
|
|
215
|
+
if (directory) {
|
|
216
|
+
this.setBaseDir(directory);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 如果指定了目录且目录不存在,创建目录
|
|
220
|
+
if (directory && !fs.existsSync(this.baseDir)) {
|
|
221
|
+
fs.mkdirSync(this.baseDir, { recursive: true });
|
|
222
|
+
console.log(chalk.green(`✓ 创建目录: ${this.baseDir}`));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (fs.existsSync(this.configPath)) {
|
|
226
|
+
const { overwrite } = await inquirer.prompt([
|
|
227
|
+
{
|
|
228
|
+
type: "confirm",
|
|
229
|
+
name: "overwrite",
|
|
230
|
+
message: "工作空间已存在,是否覆盖?",
|
|
231
|
+
default: false,
|
|
232
|
+
},
|
|
233
|
+
]);
|
|
234
|
+
if (!overwrite) {
|
|
235
|
+
console.log(chalk.yellow("已取消初始化"));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
this.ensureConfig(true); // 使用默认配置
|
|
241
|
+
console.log(chalk.green("✓ 工作空间初始化成功"));
|
|
242
|
+
console.log(chalk.gray(` 目录: ${this.baseDir}`));
|
|
243
|
+
console.log(chalk.gray(` 配置文件: ${this.configPath}`));
|
|
244
|
+
|
|
245
|
+
// 显示默认分组和预配置仓库
|
|
246
|
+
const config = this.loadConfig();
|
|
247
|
+
const groupNames = Object.keys(config.groups);
|
|
248
|
+
if (groupNames.length > 0) {
|
|
249
|
+
console.log(chalk.blue(" 默认分组:"));
|
|
250
|
+
for (const name of groupNames) {
|
|
251
|
+
const group = config.groups[name];
|
|
252
|
+
const repoCount = group.repos?.length || 0;
|
|
253
|
+
console.log(
|
|
254
|
+
chalk.gray(
|
|
255
|
+
` • ${name} (${group.code})${repoCount > 0 ? ` - ${repoCount} 个预配置仓库` : ""}`,
|
|
256
|
+
),
|
|
257
|
+
);
|
|
258
|
+
if (repoCount > 0) {
|
|
259
|
+
for (const repo of group.repos) {
|
|
260
|
+
console.log(chalk.gray(` - ${repo.name} (${repo.branch})`));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 如果指定了 AI 编辑器,生成对应的 prompt 文件
|
|
267
|
+
if (aiEditor) {
|
|
268
|
+
console.log();
|
|
269
|
+
this.generatePromptFile(aiEditor);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* 扫描工作目录下的 Git 项目
|
|
275
|
+
*/
|
|
276
|
+
async scan(interactive = true) {
|
|
277
|
+
const config = this.loadConfig();
|
|
278
|
+
const currentDir = this.baseDir;
|
|
279
|
+
const dirs = fs
|
|
280
|
+
.readdirSync(currentDir, { withFileTypes: true })
|
|
281
|
+
.filter((dirent) => dirent.isDirectory())
|
|
282
|
+
.filter(
|
|
283
|
+
(dirent) =>
|
|
284
|
+
!dirent.name.startsWith(".") && dirent.name !== "node_modules",
|
|
285
|
+
)
|
|
286
|
+
.map((dirent) => dirent.name);
|
|
287
|
+
|
|
288
|
+
const gitRepos = [];
|
|
289
|
+
for (const dir of dirs) {
|
|
290
|
+
const gitDir = path.join(currentDir, dir, ".git");
|
|
291
|
+
if (fs.existsSync(gitDir)) {
|
|
292
|
+
try {
|
|
293
|
+
const url = execSync("git remote get-url origin", {
|
|
294
|
+
cwd: path.join(currentDir, dir),
|
|
295
|
+
encoding: "utf-8",
|
|
296
|
+
}).trim();
|
|
297
|
+
|
|
298
|
+
const branch = execSync("git branch --show-current", {
|
|
299
|
+
cwd: path.join(currentDir, dir),
|
|
300
|
+
encoding: "utf-8",
|
|
301
|
+
}).trim();
|
|
302
|
+
|
|
303
|
+
gitRepos.push({
|
|
304
|
+
name: dir,
|
|
305
|
+
url,
|
|
306
|
+
branch: branch || "main",
|
|
307
|
+
});
|
|
308
|
+
} catch (error) {
|
|
309
|
+
// 忽略无法获取远程地址的项目
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (gitRepos.length === 0) {
|
|
315
|
+
console.log(chalk.yellow("未找到任何 Git 项目"));
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
console.log(chalk.blue(`\n发现 ${gitRepos.length} 个 Git 项目:`));
|
|
320
|
+
console.log();
|
|
321
|
+
|
|
322
|
+
for (const repo of gitRepos) {
|
|
323
|
+
// 检查是否已存在
|
|
324
|
+
let exists = false;
|
|
325
|
+
let existingRepo = null;
|
|
326
|
+
let existingGroup = null;
|
|
327
|
+
|
|
328
|
+
for (const [groupName, group] of Object.entries(config.groups)) {
|
|
329
|
+
const found = group.repos?.find((r) => r.name === repo.name);
|
|
330
|
+
if (found) {
|
|
331
|
+
exists = true;
|
|
332
|
+
existingRepo = found;
|
|
333
|
+
existingGroup = groupName;
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (exists) {
|
|
339
|
+
console.log(chalk.gray(` • ${repo.name} (已存在于 ${existingGroup})`));
|
|
340
|
+
// 更新分支信息
|
|
341
|
+
if (existingRepo.branch !== repo.branch) {
|
|
342
|
+
existingRepo.branch = repo.branch;
|
|
343
|
+
console.log(chalk.gray(` 分支更新为: ${repo.branch}`));
|
|
344
|
+
}
|
|
345
|
+
} else {
|
|
346
|
+
console.log(chalk.cyan(` • ${repo.name}`));
|
|
347
|
+
console.log(chalk.gray(` URL: ${repo.url}`));
|
|
348
|
+
console.log(chalk.gray(` 分支: ${repo.branch}`));
|
|
349
|
+
|
|
350
|
+
if (interactive) {
|
|
351
|
+
const answers = await inquirer.prompt([
|
|
352
|
+
{
|
|
353
|
+
type: "confirm",
|
|
354
|
+
name: "add",
|
|
355
|
+
message: `是否添加 ${repo.name}?`,
|
|
356
|
+
default: true,
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
type: "input",
|
|
360
|
+
name: "descriptions",
|
|
361
|
+
message: "请输入项目描述(多个描述用逗号分隔):",
|
|
362
|
+
when: (answers) => answers.add,
|
|
363
|
+
default: repo.name,
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
type: "list",
|
|
367
|
+
name: "group",
|
|
368
|
+
message: "选择分组:",
|
|
369
|
+
when: (answers) => answers.add,
|
|
370
|
+
choices: [
|
|
371
|
+
...Object.keys(config.groups),
|
|
372
|
+
new inquirer.Separator(),
|
|
373
|
+
"创建新分组",
|
|
374
|
+
],
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
type: "input",
|
|
378
|
+
name: "newGroupName",
|
|
379
|
+
message: "输入新分组名称:",
|
|
380
|
+
when: (answers) => answers.group === "创建新分组",
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
type: "input",
|
|
384
|
+
name: "newGroupCode",
|
|
385
|
+
message: "输入新分组代码:",
|
|
386
|
+
when: (answers) => answers.group === "创建新分组",
|
|
387
|
+
},
|
|
388
|
+
]);
|
|
389
|
+
|
|
390
|
+
if (answers.add) {
|
|
391
|
+
const descriptions = answers.descriptions
|
|
392
|
+
? answers.descriptions
|
|
393
|
+
.split(",")
|
|
394
|
+
.map((d) => d.trim())
|
|
395
|
+
.filter(Boolean)
|
|
396
|
+
: [repo.name];
|
|
397
|
+
|
|
398
|
+
let groupName =
|
|
399
|
+
answers.group === "创建新分组"
|
|
400
|
+
? answers.newGroupName
|
|
401
|
+
: answers.group;
|
|
402
|
+
|
|
403
|
+
if (answers.group === "创建新分组") {
|
|
404
|
+
config.groups[groupName] = {
|
|
405
|
+
code: answers.newGroupCode || "custom",
|
|
406
|
+
description: groupName,
|
|
407
|
+
repos: [],
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (!config.groups[groupName]) {
|
|
412
|
+
config.groups[groupName] = {
|
|
413
|
+
code: groupName.toLowerCase(),
|
|
414
|
+
description: groupName,
|
|
415
|
+
repos: [],
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
config.groups[groupName].repos.push({
|
|
420
|
+
name: repo.name,
|
|
421
|
+
url: repo.url,
|
|
422
|
+
descriptions,
|
|
423
|
+
branch: repo.branch,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
console.log(chalk.green(` ✓ 已添加到 ${groupName}`));
|
|
427
|
+
}
|
|
428
|
+
} else {
|
|
429
|
+
// 非交互模式,自动添加到 other 分组
|
|
430
|
+
if (!config.groups.other) {
|
|
431
|
+
config.groups.other = {
|
|
432
|
+
code: "other",
|
|
433
|
+
description: "未分组",
|
|
434
|
+
repos: [],
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
config.groups.other.repos.push({
|
|
438
|
+
name: repo.name,
|
|
439
|
+
url: repo.url,
|
|
440
|
+
descriptions: [repo.name],
|
|
441
|
+
branch: repo.branch,
|
|
442
|
+
});
|
|
443
|
+
console.log(chalk.green(` ✓ 自动添加到 other 分组`));
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
console.log();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
this.saveConfig(config);
|
|
450
|
+
console.log(chalk.green("✓ 扫描完成,配置已更新"));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* 列出所有项目
|
|
455
|
+
*/
|
|
456
|
+
async list(groupFilter = null) {
|
|
457
|
+
const config = this.loadConfig();
|
|
458
|
+
|
|
459
|
+
for (const [groupName, group] of Object.entries(config.groups)) {
|
|
460
|
+
if (groupFilter && groupName !== groupFilter) continue;
|
|
461
|
+
|
|
462
|
+
console.log(chalk.blue(`\n${groupName} (${group.code})`));
|
|
463
|
+
console.log(chalk.gray(group.description));
|
|
464
|
+
console.log();
|
|
465
|
+
|
|
466
|
+
if (!group.repos || group.repos.length === 0) {
|
|
467
|
+
console.log(chalk.gray(" (无项目)"));
|
|
468
|
+
} else {
|
|
469
|
+
for (const repo of group.repos) {
|
|
470
|
+
console.log(chalk.cyan(` • ${repo.name}`));
|
|
471
|
+
console.log(chalk.gray(` URL: ${repo.url}`));
|
|
472
|
+
console.log(chalk.gray(` 分支: ${repo.branch}`));
|
|
473
|
+
if (repo.descriptions && repo.descriptions.length > 0) {
|
|
474
|
+
console.log(
|
|
475
|
+
chalk.gray(` 描述: ${repo.descriptions.join(", ")}`),
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
console.log();
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* 添加项目
|
|
486
|
+
*/
|
|
487
|
+
async add(name, options) {
|
|
488
|
+
const config = this.loadConfig();
|
|
489
|
+
const { url, group, branch, description } = options;
|
|
490
|
+
|
|
491
|
+
// 检查是否已存在
|
|
492
|
+
for (const [groupName, g] of Object.entries(config.groups)) {
|
|
493
|
+
if (g.repos?.find((r) => r.name === name)) {
|
|
494
|
+
throw new Error(`项目 ${name} 已存在于 ${groupName} 分组`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (!config.groups[group]) {
|
|
499
|
+
config.groups[group] = {
|
|
500
|
+
code: group.toLowerCase(),
|
|
501
|
+
description: group,
|
|
502
|
+
repos: [],
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
config.groups[group].repos.push({
|
|
507
|
+
name,
|
|
508
|
+
url,
|
|
509
|
+
descriptions: description ? [description] : [name],
|
|
510
|
+
branch,
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
this.saveConfig(config);
|
|
514
|
+
console.log(chalk.green(`✓ 项目 ${name} 已添加到 ${group} 分组`));
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* 移除项目
|
|
519
|
+
*/
|
|
520
|
+
async remove(name) {
|
|
521
|
+
const config = this.loadConfig();
|
|
522
|
+
let removed = false;
|
|
523
|
+
|
|
524
|
+
for (const [groupName, group] of Object.entries(config.groups)) {
|
|
525
|
+
const index = group.repos?.findIndex((r) => r.name === name);
|
|
526
|
+
if (index >= 0) {
|
|
527
|
+
group.repos.splice(index, 1);
|
|
528
|
+
removed = true;
|
|
529
|
+
console.log(chalk.green(`✓ 项目 ${name} 已从 ${groupName} 分组移除`));
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (!removed) {
|
|
535
|
+
throw new Error(`项目 ${name} 不存在`);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
this.saveConfig(config);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* 克隆所有仓库
|
|
543
|
+
*/
|
|
544
|
+
async cloneAll(groupFilter = null) {
|
|
545
|
+
const config = this.loadConfig();
|
|
546
|
+
const reposToClone = [];
|
|
547
|
+
|
|
548
|
+
for (const [groupName, group] of Object.entries(config.groups)) {
|
|
549
|
+
if (groupFilter && groupName !== groupFilter) continue;
|
|
550
|
+
if (group.repos) {
|
|
551
|
+
reposToClone.push(...group.repos);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (reposToClone.length === 0) {
|
|
556
|
+
console.log(chalk.yellow("没有需要克隆的仓库"));
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
console.log(chalk.blue(`\n准备克隆 ${reposToClone.length} 个仓库:\n`));
|
|
561
|
+
|
|
562
|
+
for (const repo of reposToClone) {
|
|
563
|
+
if (fs.existsSync(repo.name)) {
|
|
564
|
+
console.log(chalk.yellow(` • ${repo.name} 已存在,跳过`));
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
console.log(chalk.cyan(` • 正在克隆 ${repo.name}...`));
|
|
569
|
+
try {
|
|
570
|
+
execSync(`git clone -b ${repo.branch} ${repo.url} ${repo.name}`, {
|
|
571
|
+
stdio: "inherit",
|
|
572
|
+
});
|
|
573
|
+
console.log(chalk.green(` ✓ 克隆成功\n`));
|
|
574
|
+
} catch (error) {
|
|
575
|
+
console.log(chalk.red(` ✗ 克隆失败\n`));
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
console.log(chalk.green("✓ 克隆任务完成"));
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* 获取 nginx-config 目录路径
|
|
584
|
+
*/
|
|
585
|
+
getNginxConfigDir() {
|
|
586
|
+
// 尝试多个可能的路径
|
|
587
|
+
const possiblePaths = [
|
|
588
|
+
path.join(__dirname, "..", "nginx-config"),
|
|
589
|
+
path.join(process.cwd(), "nginx-config"),
|
|
590
|
+
path.join(__dirname, "..", "..", "nginx-config"),
|
|
591
|
+
];
|
|
592
|
+
|
|
593
|
+
for (const dir of possiblePaths) {
|
|
594
|
+
if (fs.existsSync(dir)) {
|
|
595
|
+
return dir;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* 列出可用的 nginx 配置
|
|
604
|
+
*/
|
|
605
|
+
async listNginxConfigs() {
|
|
606
|
+
const nginxDir = this.getNginxConfigDir();
|
|
607
|
+
|
|
608
|
+
if (!nginxDir) {
|
|
609
|
+
console.log(chalk.yellow("⚠ 未找到 nginx-config 目录"));
|
|
610
|
+
return [];
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const files = fs
|
|
614
|
+
.readdirSync(nginxDir)
|
|
615
|
+
.filter((f) => f.endsWith(".conf"))
|
|
616
|
+
.sort();
|
|
617
|
+
|
|
618
|
+
if (files.length === 0) {
|
|
619
|
+
console.log(chalk.yellow("⚠ 未找到 .conf 配置文件"));
|
|
620
|
+
return [];
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
console.log(chalk.blue("\n可用的 nginx 配置:\n"));
|
|
624
|
+
|
|
625
|
+
files.forEach((file, index) => {
|
|
626
|
+
const filePath = path.join(nginxDir, file);
|
|
627
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
628
|
+
const portMatch = content.match(/listen\s+(\d+)/);
|
|
629
|
+
const port = portMatch ? portMatch[1] : "unknown";
|
|
630
|
+
|
|
631
|
+
console.log(chalk.cyan(` ${index + 1}. ${file}`));
|
|
632
|
+
console.log(chalk.gray(` 端口: ${port}`));
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
console.log();
|
|
636
|
+
console.log(chalk.gray("使用方式:"));
|
|
637
|
+
console.log(
|
|
638
|
+
chalk.gray(" wsc nginx -c 5006_plat # 复制配置到当前目录"),
|
|
639
|
+
);
|
|
640
|
+
console.log(
|
|
641
|
+
chalk.gray(" wsc nginx -s 5006_plat # 复制并启动 nginx"),
|
|
642
|
+
);
|
|
643
|
+
console.log(
|
|
644
|
+
chalk.gray(" .\\start-nginx.bat # 直接运行启动脚本"),
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
return files;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* 初始化当前工作空间的 nginx 环境
|
|
652
|
+
* 下载 nginx Windows 绿色版,创建独立配置,解决多空间冲突
|
|
653
|
+
*/
|
|
654
|
+
async initNginx(options = {}) {
|
|
655
|
+
const {
|
|
656
|
+
version = "1.24.0",
|
|
657
|
+
skipDownload = false,
|
|
658
|
+
proxyTarget = "http://192.168.10.189",
|
|
659
|
+
} = options;
|
|
660
|
+
|
|
661
|
+
console.log(chalk.blue("\n========================================"));
|
|
662
|
+
console.log(chalk.blue(" 初始化工作空间 Nginx 环境"));
|
|
663
|
+
console.log(chalk.blue("========================================\n"));
|
|
664
|
+
|
|
665
|
+
// 确保工作空间已初始化
|
|
666
|
+
this.ensureConfig(true);
|
|
667
|
+
|
|
668
|
+
// 加载配置
|
|
669
|
+
const config = this.loadConfig();
|
|
670
|
+
|
|
671
|
+
// 检查是否已存在 nginx 配置
|
|
672
|
+
if (config.nginx?.installed) {
|
|
673
|
+
const { reinitialize } = await inquirer.prompt([
|
|
674
|
+
{
|
|
675
|
+
type: "confirm",
|
|
676
|
+
name: "reinitialize",
|
|
677
|
+
message: `Nginx is already initialized (version: ${config.nginx.version}). Reinitialize?`,
|
|
678
|
+
default: false,
|
|
679
|
+
},
|
|
680
|
+
]);
|
|
681
|
+
|
|
682
|
+
if (!reinitialize) {
|
|
683
|
+
console.log(chalk.yellow("Initialization cancelled."));
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
console.log(chalk.yellow("Removing existing nginx configuration..."));
|
|
688
|
+
// 停止当前 nginx
|
|
689
|
+
const nginxDir = path.join(this.workspaceDir, "nginx");
|
|
690
|
+
if (fs.existsSync(nginxDir)) {
|
|
691
|
+
try {
|
|
692
|
+
// 尝试停止 nginx
|
|
693
|
+
const stopScript = path.join(nginxDir, "stop-nginx.bat");
|
|
694
|
+
if (fs.existsSync(stopScript)) {
|
|
695
|
+
execSync(`"${stopScript}"`, { stdio: "ignore", timeout: 10000 });
|
|
696
|
+
}
|
|
697
|
+
} catch {
|
|
698
|
+
// 忽略停止错误
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
console.log(chalk.gray(` 代理后端地址: ${proxyTarget}`));
|
|
704
|
+
|
|
705
|
+
// 初始化 nginx 配置结构
|
|
706
|
+
if (!config.nginx) {
|
|
707
|
+
config.nginx = {};
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// 设置工作空间 nginx 配置
|
|
711
|
+
const workspaceNginxConfig = {
|
|
712
|
+
version: version,
|
|
713
|
+
proxyTarget: proxyTarget,
|
|
714
|
+
installed: false,
|
|
715
|
+
configs: [],
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
config.nginx = workspaceNginxConfig;
|
|
719
|
+
this.saveConfig(config);
|
|
720
|
+
|
|
721
|
+
console.log(chalk.gray(` 工作空间: ${this.baseDir}`));
|
|
722
|
+
console.log();
|
|
723
|
+
|
|
724
|
+
// 定义目录结构
|
|
725
|
+
const nginxDir = path.join(this.workspaceDir, "nginx");
|
|
726
|
+
const nginxBinDir = path.join(nginxDir, "nginx-" + version);
|
|
727
|
+
// 配置文件放在 nginx/conf/conf.d/ 下,使用相对路径
|
|
728
|
+
const configDir = path.join(nginxBinDir, "conf", "conf.d");
|
|
729
|
+
// 使用 nginx 自带的 logs 和 temp 目录(相对路径)
|
|
730
|
+
const logsDir = "logs";
|
|
731
|
+
const tempDir = "temp";
|
|
732
|
+
|
|
733
|
+
// 注意:conf.d 目录在 nginx 解压后才会创建,这里不提前创建
|
|
734
|
+
// 因为 mkdirSync 的 recursive: true 会连带创建 nginx-1.24.0 目录
|
|
735
|
+
// 导致 downloadNginx 误以为 nginx 已安装
|
|
736
|
+
|
|
737
|
+
// 下载 nginx
|
|
738
|
+
if (!skipDownload) {
|
|
739
|
+
await this.downloadNginx(version, nginxDir, nginxBinDir);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// 下载完成后,创建 conf.d 目录
|
|
743
|
+
if (!fs.existsSync(configDir)) {
|
|
744
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
745
|
+
console.log(chalk.green(`✓ 创建配置目录: ${configDir}`));
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// 生成 nginx 主配置文件
|
|
749
|
+
await this.generateNginxMainConfig(nginxBinDir);
|
|
750
|
+
|
|
751
|
+
// 复制 nginx 项目配置到 conf.d(替换占位符,init 时不替换 serveport)
|
|
752
|
+
await this.copyNginxConfigsToConfigD(
|
|
753
|
+
configDir,
|
|
754
|
+
proxyTarget,
|
|
755
|
+
null, // serveport 在启动时动态分配
|
|
756
|
+
);
|
|
757
|
+
|
|
758
|
+
// 复制并修改管理脚本(PowerShell 版本)
|
|
759
|
+
await this.copyNginxPowerShellScripts(
|
|
760
|
+
nginxDir,
|
|
761
|
+
nginxBinDir,
|
|
762
|
+
configDir,
|
|
763
|
+
proxyTarget,
|
|
764
|
+
);
|
|
765
|
+
|
|
766
|
+
// 更新配置
|
|
767
|
+
config.nginx.installed = true;
|
|
768
|
+
config.nginx.binDir = nginxBinDir;
|
|
769
|
+
config.nginx.configDir = configDir;
|
|
770
|
+
config.nginx.logsDir = logsDir;
|
|
771
|
+
|
|
772
|
+
// 扫描 conf.d 目录下的配置
|
|
773
|
+
const configFiles = fs
|
|
774
|
+
.readdirSync(configDir)
|
|
775
|
+
.filter((f) => f.endsWith(".conf"))
|
|
776
|
+
.map((f) => ({
|
|
777
|
+
name: f.replace(".conf", ""),
|
|
778
|
+
file: f,
|
|
779
|
+
port: this.extractPortFromConfig(path.join(configDir, f)),
|
|
780
|
+
}));
|
|
781
|
+
config.nginx.configs = configFiles;
|
|
782
|
+
|
|
783
|
+
this.saveConfig(config);
|
|
784
|
+
|
|
785
|
+
console.log();
|
|
786
|
+
console.log(chalk.green("✓ Nginx 初始化完成"));
|
|
787
|
+
console.log();
|
|
788
|
+
console.log(chalk.blue("工作空间信息:"));
|
|
789
|
+
console.log(chalk.gray(` 代理: ${proxyTarget}`));
|
|
790
|
+
console.log();
|
|
791
|
+
|
|
792
|
+
console.log(chalk.blue("工作空间 nginx 命令:"));
|
|
793
|
+
console.log(chalk.gray(` wsc nginx start # 启动当前空间的 nginx`));
|
|
794
|
+
console.log(chalk.gray(` wsc nginx stop # 停止当前空间的 nginx`));
|
|
795
|
+
console.log(chalk.gray(` wsc nginx reload # 重载当前空间的 nginx`));
|
|
796
|
+
console.log(chalk.gray(` wsc nginx status # 查看当前空间的 nginx 状态`));
|
|
797
|
+
console.log();
|
|
798
|
+
console.log(chalk.blue("生成的 PowerShell 脚本(已配置环境变量):"));
|
|
799
|
+
console.log(chalk.gray(` ${path.join(nginxDir, "start-nginx.ps1")}`));
|
|
800
|
+
console.log(chalk.gray(` ${path.join(nginxDir, "stop-nginx.ps1")}`));
|
|
801
|
+
console.log(chalk.gray(` ${path.join(nginxDir, "reload-nginx.ps1")}`));
|
|
802
|
+
console.log(chalk.gray(` ${path.join(nginxDir, "status-nginx.ps1")}`));
|
|
803
|
+
console.log();
|
|
804
|
+
console.log(
|
|
805
|
+
chalk.yellow("注意:请勿直接运行 nginx-config 目录下的模板脚本!"),
|
|
806
|
+
);
|
|
807
|
+
console.log();
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* 查找可用的基础端口(解决多空间冲突)
|
|
812
|
+
*/
|
|
813
|
+
findAvailableBasePort(currentConfig) {
|
|
814
|
+
// 默认起始端口 50000
|
|
815
|
+
const defaultBasePort = 50000;
|
|
816
|
+
|
|
817
|
+
// 扫描工作空间目录查找已使用的端口
|
|
818
|
+
const usedPorts = new Set();
|
|
819
|
+
|
|
820
|
+
try {
|
|
821
|
+
const parentDir = path.dirname(this.baseDir);
|
|
822
|
+
const entries = fs.readdirSync(parentDir, { withFileTypes: true });
|
|
823
|
+
|
|
824
|
+
for (const entry of entries) {
|
|
825
|
+
if (entry.isDirectory()) {
|
|
826
|
+
const otherWorkspaceDir = path.join(
|
|
827
|
+
parentDir,
|
|
828
|
+
entry.name,
|
|
829
|
+
".workspace",
|
|
830
|
+
);
|
|
831
|
+
const otherConfigPath = path.join(otherWorkspaceDir, "config.json");
|
|
832
|
+
|
|
833
|
+
if (
|
|
834
|
+
fs.existsSync(otherConfigPath) &&
|
|
835
|
+
otherConfigPath !== this.configPath
|
|
836
|
+
) {
|
|
837
|
+
try {
|
|
838
|
+
const otherConfig = JSON.parse(
|
|
839
|
+
fs.readFileSync(otherConfigPath, "utf-8"),
|
|
840
|
+
);
|
|
841
|
+
if (otherConfig.nginx?.basePort) {
|
|
842
|
+
usedPorts.add(otherConfig.nginx.basePort);
|
|
843
|
+
}
|
|
844
|
+
} catch {
|
|
845
|
+
// 忽略读取错误的配置
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
} catch {
|
|
851
|
+
// 忽略扫描错误
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// 如果当前配置已有端口且未被其他空间使用,则保留
|
|
855
|
+
if (
|
|
856
|
+
currentConfig.nginx?.basePort &&
|
|
857
|
+
!usedPorts.has(currentConfig.nginx.basePort)
|
|
858
|
+
) {
|
|
859
|
+
return currentConfig.nginx.basePort;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// 查找可用端口
|
|
863
|
+
let basePort = defaultBasePort;
|
|
864
|
+
while (usedPorts.has(basePort)) {
|
|
865
|
+
basePort += 1000; // 每个空间间隔1000个端口
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
return basePort;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* 从配置文件中提取端口
|
|
873
|
+
*/
|
|
874
|
+
extractPortFromConfig(configPath) {
|
|
875
|
+
try {
|
|
876
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
877
|
+
const match = content.match(/listen\s+(\d+)/);
|
|
878
|
+
return match ? parseInt(match[1]) : null;
|
|
879
|
+
} catch {
|
|
880
|
+
return null;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* 获取 nginx 全局缓存目录
|
|
886
|
+
*/
|
|
887
|
+
getNginxCacheDir() {
|
|
888
|
+
// 尝试多种方式获取用户主目录
|
|
889
|
+
const homeDir =
|
|
890
|
+
process.env.HOME ||
|
|
891
|
+
process.env.USERPROFILE ||
|
|
892
|
+
process.env.HOMEDRIVE + process.env.HOMEPATH ||
|
|
893
|
+
(process.platform === "win32"
|
|
894
|
+
? "C:\\Users\\" + process.env.USERNAME
|
|
895
|
+
: null);
|
|
896
|
+
|
|
897
|
+
if (!homeDir) {
|
|
898
|
+
console.log(chalk.yellow(" ⚠ 无法确定用户主目录,将使用本地缓存"));
|
|
899
|
+
return null;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
return path.join(homeDir, ".wsc-cache", "nginx");
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* 下载 nginx Windows 绿色版(支持全局缓存)
|
|
907
|
+
*/
|
|
908
|
+
async downloadNginx(version, nginxDir, nginxBinDir) {
|
|
909
|
+
console.log(chalk.blue("\n下载 nginx..."));
|
|
910
|
+
|
|
911
|
+
// 检查是否已存在
|
|
912
|
+
if (fs.existsSync(nginxBinDir)) {
|
|
913
|
+
console.log(chalk.yellow(`⚠ nginx ${version} 已存在于 ${nginxBinDir}`));
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
const zipFileName = `nginx-${version}.zip`;
|
|
918
|
+
const zipPath = path.join(nginxDir, zipFileName);
|
|
919
|
+
|
|
920
|
+
// 获取全局缓存目录
|
|
921
|
+
const cacheDir = this.getNginxCacheDir();
|
|
922
|
+
console.log(
|
|
923
|
+
chalk.gray(` 缓存目录: ${cacheDir || "无法确定(使用本地目录)"}`),
|
|
924
|
+
);
|
|
925
|
+
|
|
926
|
+
// 确保缓存目录存在(如果可能)
|
|
927
|
+
if (cacheDir && !fs.existsSync(cacheDir)) {
|
|
928
|
+
try {
|
|
929
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
930
|
+
console.log(chalk.gray(` ✓ 创建缓存目录: ${cacheDir}`));
|
|
931
|
+
} catch (error) {
|
|
932
|
+
console.log(chalk.yellow(` ⚠ 无法创建缓存目录: ${error.message}`));
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const cacheZipPath = cacheDir ? path.join(cacheDir, zipFileName) : null;
|
|
937
|
+
|
|
938
|
+
// 检查本地是否已下载且文件有效(大于1MB)
|
|
939
|
+
const isValidLocalDownload =
|
|
940
|
+
fs.existsSync(zipPath) && fs.statSync(zipPath).size > 1024 * 1024;
|
|
941
|
+
|
|
942
|
+
// 检查缓存是否存在且有效
|
|
943
|
+
const isValidCache =
|
|
944
|
+
cacheZipPath &&
|
|
945
|
+
fs.existsSync(cacheZipPath) &&
|
|
946
|
+
fs.statSync(cacheZipPath).size > 1024 * 1024;
|
|
947
|
+
|
|
948
|
+
let hasValidZip = isValidLocalDownload;
|
|
949
|
+
|
|
950
|
+
if (isValidLocalDownload) {
|
|
951
|
+
console.log(chalk.yellow(`⚠ 安装包已存在: ${zipPath}`));
|
|
952
|
+
} else if (isValidCache) {
|
|
953
|
+
// 从缓存复制到本地
|
|
954
|
+
console.log(chalk.gray(` 从缓存复制: ${cacheZipPath}`));
|
|
955
|
+
try {
|
|
956
|
+
// 确保目标目录存在
|
|
957
|
+
if (!fs.existsSync(nginxDir)) {
|
|
958
|
+
fs.mkdirSync(nginxDir, { recursive: true });
|
|
959
|
+
}
|
|
960
|
+
fs.copyFileSync(cacheZipPath, zipPath);
|
|
961
|
+
hasValidZip = true;
|
|
962
|
+
console.log(chalk.green(` ✓ 从缓存复制成功`));
|
|
963
|
+
} catch (error) {
|
|
964
|
+
console.log(
|
|
965
|
+
chalk.yellow(` ⚠ 复制缓存失败,将重新下载: ${error.message}`),
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// 如果本地仍然没有有效的 zip 文件,则下载
|
|
971
|
+
if (
|
|
972
|
+
!hasValidZip &&
|
|
973
|
+
(!fs.existsSync(zipPath) || fs.statSync(zipPath).size <= 1024 * 1024)
|
|
974
|
+
) {
|
|
975
|
+
// 删除可能存在的不完整文件
|
|
976
|
+
if (fs.existsSync(zipPath)) {
|
|
977
|
+
fs.unlinkSync(zipPath);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// 下载 nginx
|
|
981
|
+
const downloadUrl = `https://github.com/nginx/nginx/releases/download/release-${version}/${zipFileName}`;
|
|
982
|
+
const backupUrl = `http://nginx.org/download/nginx-${version}.zip`;
|
|
983
|
+
|
|
984
|
+
let downloaded = false;
|
|
985
|
+
|
|
986
|
+
// 尝试主下载地址 (nginx.org)
|
|
987
|
+
try {
|
|
988
|
+
console.log(chalk.gray(` 尝试从 nginx.org 下载...`));
|
|
989
|
+
// 先下载到缓存目录
|
|
990
|
+
const downloadTarget = cacheZipPath || zipPath;
|
|
991
|
+
execSync(`curl -L -o "${downloadTarget}" "${backupUrl}"`, {
|
|
992
|
+
stdio: "inherit",
|
|
993
|
+
timeout: 180000,
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
// 验证下载是否成功
|
|
997
|
+
if (
|
|
998
|
+
fs.existsSync(downloadTarget) &&
|
|
999
|
+
fs.statSync(downloadTarget).size > 1024 * 1024
|
|
1000
|
+
) {
|
|
1001
|
+
downloaded = true;
|
|
1002
|
+
console.log(chalk.green(` ✓ 从 nginx.org 下载成功`));
|
|
1003
|
+
// 如果下载到缓存,同时复制到本地
|
|
1004
|
+
if (cacheZipPath && downloadTarget === cacheZipPath) {
|
|
1005
|
+
// 确保目标目录存在
|
|
1006
|
+
if (!fs.existsSync(nginxDir)) {
|
|
1007
|
+
fs.mkdirSync(nginxDir, { recursive: true });
|
|
1008
|
+
}
|
|
1009
|
+
fs.copyFileSync(cacheZipPath, zipPath);
|
|
1010
|
+
}
|
|
1011
|
+
} else {
|
|
1012
|
+
console.log(chalk.yellow(" ✗ 下载文件无效或太小,尝试备用源..."));
|
|
1013
|
+
}
|
|
1014
|
+
} catch (error) {
|
|
1015
|
+
console.log(
|
|
1016
|
+
chalk.yellow(
|
|
1017
|
+
` ✗ nginx.org 下载失败: ${error.message || "网络错误"}`,
|
|
1018
|
+
),
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// 如果主源失败,尝试 GitHub
|
|
1023
|
+
if (!downloaded) {
|
|
1024
|
+
// 清理失败的文件
|
|
1025
|
+
if (cacheZipPath && fs.existsSync(cacheZipPath)) {
|
|
1026
|
+
fs.unlinkSync(cacheZipPath);
|
|
1027
|
+
}
|
|
1028
|
+
if (fs.existsSync(zipPath)) {
|
|
1029
|
+
fs.unlinkSync(zipPath);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
try {
|
|
1033
|
+
console.log(chalk.gray(` 尝试从 GitHub 下载...`));
|
|
1034
|
+
const downloadTarget = cacheZipPath || zipPath;
|
|
1035
|
+
execSync(`curl -L -o "${downloadTarget}" "${downloadUrl}"`, {
|
|
1036
|
+
stdio: "inherit",
|
|
1037
|
+
timeout: 180000,
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
// 验证下载是否成功
|
|
1041
|
+
if (
|
|
1042
|
+
fs.existsSync(downloadTarget) &&
|
|
1043
|
+
fs.statSync(downloadTarget).size > 1024 * 1024
|
|
1044
|
+
) {
|
|
1045
|
+
downloaded = true;
|
|
1046
|
+
console.log(chalk.green(` ✓ 从 GitHub 下载成功`));
|
|
1047
|
+
// 如果下载到缓存,同时复制到本地
|
|
1048
|
+
if (cacheZipPath && downloadTarget === cacheZipPath) {
|
|
1049
|
+
// 确保目标目录存在
|
|
1050
|
+
if (!fs.existsSync(nginxDir)) {
|
|
1051
|
+
fs.mkdirSync(nginxDir, { recursive: true });
|
|
1052
|
+
}
|
|
1053
|
+
fs.copyFileSync(cacheZipPath, zipPath);
|
|
1054
|
+
}
|
|
1055
|
+
} else {
|
|
1056
|
+
console.log(chalk.yellow(" ✗ 下载文件无效或太小"));
|
|
1057
|
+
}
|
|
1058
|
+
} catch (error) {
|
|
1059
|
+
console.log(
|
|
1060
|
+
chalk.yellow(` ✗ GitHub 下载失败: ${error.message || "网络错误"}`),
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
if (!downloaded) {
|
|
1066
|
+
console.log(chalk.red("\n✗ 下载失败,请手动下载并解压到:"));
|
|
1067
|
+
console.log(chalk.gray(` ${nginxBinDir}`));
|
|
1068
|
+
console.log(chalk.gray(` 下载地址: ${backupUrl}`));
|
|
1069
|
+
throw new Error("nginx 下载失败");
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// 解压
|
|
1074
|
+
console.log(chalk.gray(" 解压中..."));
|
|
1075
|
+
try {
|
|
1076
|
+
// 使用 PowerShell 解压
|
|
1077
|
+
execSync(
|
|
1078
|
+
`powershell -Command "Expand-Archive -Path '${zipPath}' -DestinationPath '${nginxDir}' -Force"`,
|
|
1079
|
+
{
|
|
1080
|
+
stdio: "pipe",
|
|
1081
|
+
timeout: 60000,
|
|
1082
|
+
},
|
|
1083
|
+
);
|
|
1084
|
+
console.log(chalk.green(`✓ nginx ${version} 已安装到 ${nginxBinDir}`));
|
|
1085
|
+
} catch (error) {
|
|
1086
|
+
console.log(chalk.red("✗ 解压失败"));
|
|
1087
|
+
throw error;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// 删除本地 zip 文件(缓存目录保留)
|
|
1091
|
+
try {
|
|
1092
|
+
fs.unlinkSync(zipPath);
|
|
1093
|
+
} catch {
|
|
1094
|
+
// 忽略删除错误
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
/**
|
|
1099
|
+
* 生成 nginx 主配置文件
|
|
1100
|
+
*/
|
|
1101
|
+
async generateNginxMainConfig(nginxBinDir) {
|
|
1102
|
+
console.log(chalk.blue("\n生成 nginx 主配置..."));
|
|
1103
|
+
|
|
1104
|
+
const mainConfig = `worker_processes 1;
|
|
1105
|
+
|
|
1106
|
+
error_log logs/error.log;
|
|
1107
|
+
pid logs/nginx.pid;
|
|
1108
|
+
|
|
1109
|
+
events {
|
|
1110
|
+
worker_connections 1024;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
http {
|
|
1114
|
+
include mime.types;
|
|
1115
|
+
default_type application/octet-stream;
|
|
1116
|
+
|
|
1117
|
+
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
|
1118
|
+
'$status $body_bytes_sent "$http_referer" '
|
|
1119
|
+
'"$http_user_agent" "$http_x_forwarded_for"';
|
|
1120
|
+
|
|
1121
|
+
access_log logs/access.log main;
|
|
1122
|
+
|
|
1123
|
+
sendfile on;
|
|
1124
|
+
keepalive_timeout 65;
|
|
1125
|
+
|
|
1126
|
+
# 包含 conf.d 目录下的所有配置(使用相对路径)
|
|
1127
|
+
include conf.d/*.conf;
|
|
1128
|
+
}
|
|
1129
|
+
`;
|
|
1130
|
+
|
|
1131
|
+
const mainConfigPath = path.join(nginxBinDir, "conf", "nginx.conf");
|
|
1132
|
+
|
|
1133
|
+
// 确保 conf 目录存在
|
|
1134
|
+
const confDir = path.join(nginxBinDir, "conf");
|
|
1135
|
+
if (!fs.existsSync(confDir)) {
|
|
1136
|
+
fs.mkdirSync(confDir, { recursive: true });
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
fs.writeFileSync(mainConfigPath, mainConfig, "utf-8");
|
|
1140
|
+
console.log(chalk.green(`✓ 生成主配置: ${mainConfigPath}`));
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
/**
|
|
1144
|
+
* 复制 nginx 配置到 conf.d 目录
|
|
1145
|
+
* 替换占位符: <serveport>, <workspaceDir>, <proxyTarget>
|
|
1146
|
+
*/
|
|
1147
|
+
async copyNginxConfigsToConfigD(configDir, proxyTarget, serveport) {
|
|
1148
|
+
console.log(chalk.blue("\n复制 nginx 项目配置..."));
|
|
1149
|
+
|
|
1150
|
+
const sourceDir = this.getNginxConfigDir();
|
|
1151
|
+
|
|
1152
|
+
if (!sourceDir) {
|
|
1153
|
+
console.log(chalk.yellow("⚠ 未找到源 nginx-config 目录,跳过配置复制"));
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
const files = fs.readdirSync(sourceDir).filter((f) => f.endsWith(".conf"));
|
|
1158
|
+
|
|
1159
|
+
if (files.length === 0) {
|
|
1160
|
+
console.log(chalk.yellow("⚠ 未找到 .conf 配置文件"));
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// 准备替换变量
|
|
1165
|
+
const workspaceDir = this.baseDir.replace(/\\/g, "/");
|
|
1166
|
+
|
|
1167
|
+
let copiedCount = 0;
|
|
1168
|
+
for (const file of files) {
|
|
1169
|
+
const srcPath = path.join(sourceDir, file);
|
|
1170
|
+
const dstPath = path.join(configDir, file);
|
|
1171
|
+
|
|
1172
|
+
// 读取并调整配置
|
|
1173
|
+
let content = fs.readFileSync(srcPath, "utf-8");
|
|
1174
|
+
|
|
1175
|
+
// 替换占位符
|
|
1176
|
+
content = this.replaceNginxPlaceholders(
|
|
1177
|
+
content,
|
|
1178
|
+
workspaceDir,
|
|
1179
|
+
proxyTarget,
|
|
1180
|
+
serveport,
|
|
1181
|
+
);
|
|
1182
|
+
|
|
1183
|
+
fs.writeFileSync(dstPath, content, "utf-8");
|
|
1184
|
+
copiedCount++;
|
|
1185
|
+
console.log(chalk.green(`✓ 复制配置: ${file}`));
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
console.log(chalk.gray(` 共复制 ${copiedCount} 个配置`));
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/**
|
|
1192
|
+
* 查找可用的 PowerShell 命令
|
|
1193
|
+
* 优先使用 pwsh (PowerShell 7),找不到则使用 powershell (Windows PowerShell 5.1)
|
|
1194
|
+
*/
|
|
1195
|
+
findPowerShellCmd() {
|
|
1196
|
+
const fs = require("fs");
|
|
1197
|
+
const path = require("path");
|
|
1198
|
+
|
|
1199
|
+
// 常见 PowerShell 7 安装路径
|
|
1200
|
+
const ps7Paths = [
|
|
1201
|
+
"C:\\Program Files\\PowerShell\\7\\pwsh.exe",
|
|
1202
|
+
"C:\\Program Files (x86)\\PowerShell\\7\\pwsh.exe",
|
|
1203
|
+
path.join(process.env.LOCALAPPDATA || "", "Microsoft\\PowerShell\\pwsh.exe"),
|
|
1204
|
+
];
|
|
1205
|
+
|
|
1206
|
+
// 检查默认路径
|
|
1207
|
+
for (const psPath of ps7Paths) {
|
|
1208
|
+
if (fs.existsSync(psPath)) {
|
|
1209
|
+
return `"${psPath}"`;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// 尝试从 PATH 找
|
|
1214
|
+
try {
|
|
1215
|
+
execSync("where pwsh", { stdio: "ignore" });
|
|
1216
|
+
return "pwsh";
|
|
1217
|
+
} catch {
|
|
1218
|
+
// 回退到 Windows PowerShell 5.1
|
|
1219
|
+
return "powershell";
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
/**
|
|
1224
|
+
* 检测从指定端口开始的第一个可用端口
|
|
1225
|
+
* 使用连接测试方式,更可靠地检测端口是否被占用
|
|
1226
|
+
*/
|
|
1227
|
+
findAvailablePort(startPort = 5000) {
|
|
1228
|
+
const net = require("net");
|
|
1229
|
+
|
|
1230
|
+
return new Promise((resolve, reject) => {
|
|
1231
|
+
const tryPort = (port) => {
|
|
1232
|
+
// 方法1: 尝试连接该端口,如果连得上说明已被占用
|
|
1233
|
+
const client = new net.Socket();
|
|
1234
|
+
client.setTimeout(500);
|
|
1235
|
+
|
|
1236
|
+
client.once("connect", () => {
|
|
1237
|
+
// 连接成功,端口被占用
|
|
1238
|
+
client.destroy();
|
|
1239
|
+
tryPort(port + 1);
|
|
1240
|
+
});
|
|
1241
|
+
|
|
1242
|
+
client.once("error", (err) => {
|
|
1243
|
+
// 连接失败,端口可用
|
|
1244
|
+
if (err.code === "ECONNREFUSED" || err.code === "ETIMEDOUT") {
|
|
1245
|
+
client.destroy();
|
|
1246
|
+
resolve(port);
|
|
1247
|
+
} else {
|
|
1248
|
+
client.destroy();
|
|
1249
|
+
tryPort(port + 1);
|
|
1250
|
+
}
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
client.once("timeout", () => {
|
|
1254
|
+
// 超时,端口可能可用
|
|
1255
|
+
client.destroy();
|
|
1256
|
+
resolve(port);
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
client.connect(port, "127.0.0.1");
|
|
1260
|
+
};
|
|
1261
|
+
|
|
1262
|
+
tryPort(startPort);
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* 替换 nginx 配置文件中的占位符
|
|
1268
|
+
*/
|
|
1269
|
+
replaceNginxPlaceholders(content, workspaceDir, proxyTarget, serveport) {
|
|
1270
|
+
// 替换 <workspaceDir> 为工作空间根目录
|
|
1271
|
+
content = content.replace(/<workspaceDir>/gi, workspaceDir);
|
|
1272
|
+
|
|
1273
|
+
// 替换 <proxyTarget> 为代理后端地址
|
|
1274
|
+
content = content.replace(/<proxyTarget>/gi, proxyTarget);
|
|
1275
|
+
|
|
1276
|
+
// 替换 <serveport> 为服务端口
|
|
1277
|
+
if (serveport) {
|
|
1278
|
+
content = content.replace(/<serveport>/gi, serveport);
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
return content;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/**
|
|
1285
|
+
* 复制 nginx PowerShell 管理脚本并修改路径
|
|
1286
|
+
*/
|
|
1287
|
+
async copyNginxPowerShellScripts(
|
|
1288
|
+
nginxDir,
|
|
1289
|
+
nginxBinDir,
|
|
1290
|
+
configDir,
|
|
1291
|
+
proxyTarget,
|
|
1292
|
+
) {
|
|
1293
|
+
console.log(chalk.blue("\n复制 PowerShell 管理脚本..."));
|
|
1294
|
+
|
|
1295
|
+
const sourceDir = this.getNginxConfigDir();
|
|
1296
|
+
if (!sourceDir) {
|
|
1297
|
+
console.log(chalk.yellow("⚠ 未找到源 nginx-config 目录,跳过脚本复制"));
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
const nginxExe = path.join(nginxBinDir, "nginx.exe");
|
|
1302
|
+
const mainConfig = path.join(nginxBinDir, "conf", "nginx.conf");
|
|
1303
|
+
|
|
1304
|
+
// PowerShell 脚本文件列表
|
|
1305
|
+
const scriptFiles = [
|
|
1306
|
+
{ name: "start-nginx.ps1", title: "启动" },
|
|
1307
|
+
{ name: "stop-nginx.ps1", title: "停止" },
|
|
1308
|
+
{ name: "reload-nginx.ps1", title: "重载配置" },
|
|
1309
|
+
{ name: "status-nginx.ps1", title: "状态检查" },
|
|
1310
|
+
];
|
|
1311
|
+
|
|
1312
|
+
for (const scriptInfo of scriptFiles) {
|
|
1313
|
+
const scriptFile = scriptInfo.name;
|
|
1314
|
+
const sourcePath = path.join(sourceDir, scriptFile);
|
|
1315
|
+
const targetPath = path.join(nginxDir, scriptFile);
|
|
1316
|
+
|
|
1317
|
+
if (!fs.existsSync(sourcePath)) {
|
|
1318
|
+
console.log(chalk.yellow(`⚠ 未找到源脚本: ${scriptFile}`));
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// 读取源脚本
|
|
1323
|
+
let content = fs.readFileSync(sourcePath, "utf-8");
|
|
1324
|
+
|
|
1325
|
+
// 修改脚本,使用工作空间的路径和环境变量
|
|
1326
|
+
// 注意:传入 nginxBinDir 而不是 nginxDir,因为环境变量需要指向具体的 nginx 版本目录
|
|
1327
|
+
content = this.patchPowerShellScript(
|
|
1328
|
+
content,
|
|
1329
|
+
nginxExe,
|
|
1330
|
+
mainConfig,
|
|
1331
|
+
configDir,
|
|
1332
|
+
nginxBinDir,
|
|
1333
|
+
proxyTarget,
|
|
1334
|
+
scriptInfo.title,
|
|
1335
|
+
);
|
|
1336
|
+
|
|
1337
|
+
// 写入修改后的脚本
|
|
1338
|
+
fs.writeFileSync(targetPath, content, "utf-8");
|
|
1339
|
+
console.log(chalk.green(`✓ 复制并修改脚本: ${scriptFile}`));
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
/**
|
|
1344
|
+
* 修改 PowerShell 脚本,使用工作空间的路径
|
|
1345
|
+
*/
|
|
1346
|
+
patchPowerShellScript(
|
|
1347
|
+
content,
|
|
1348
|
+
nginxExe,
|
|
1349
|
+
mainConfig,
|
|
1350
|
+
configDir,
|
|
1351
|
+
nginxDir,
|
|
1352
|
+
proxyTarget,
|
|
1353
|
+
scriptTitle,
|
|
1354
|
+
) {
|
|
1355
|
+
// 将路径转换为 PowerShell 格式
|
|
1356
|
+
const nginxExeWin = nginxExe.replace(/\\/g, "\\");
|
|
1357
|
+
const mainConfigWin = mainConfig.replace(/\\/g, "\\");
|
|
1358
|
+
const nginxDirWin = nginxDir.replace(/\\/g, "\\");
|
|
1359
|
+
const workspaceDirWin = this.baseDir.replace(/\\/g, "\\");
|
|
1360
|
+
|
|
1361
|
+
// 替换环境变量默认值
|
|
1362
|
+
const envSetup = `
|
|
1363
|
+
# 工作空间配置
|
|
1364
|
+
$env:NGINX_EXE = '${nginxExeWin}'
|
|
1365
|
+
$env:NGINX_CONF = '${mainConfigWin}'
|
|
1366
|
+
$env:NGINX_DIR = '${nginxDirWin}'
|
|
1367
|
+
$env:WORKSPACE_DIR = '${workspaceDirWin}'
|
|
1368
|
+
$env:PROXY_TARGET = '${proxyTarget}'
|
|
1369
|
+
`;
|
|
1370
|
+
|
|
1371
|
+
// 在文件开头添加环境变量设置(在所有 #Requires 之后)
|
|
1372
|
+
// 匹配所有的 #Requires 行,并在最后一个之后插入环境变量
|
|
1373
|
+
const requiresMatch = content.match(/(#Requires.*\n)+/);
|
|
1374
|
+
if (requiresMatch) {
|
|
1375
|
+
// 在所有 #Requires 之后插入环境变量
|
|
1376
|
+
content = content.replace(
|
|
1377
|
+
requiresMatch[0],
|
|
1378
|
+
requiresMatch[0] + "\n" + envSetup + "\n",
|
|
1379
|
+
);
|
|
1380
|
+
} else {
|
|
1381
|
+
// 如果没有 #Requires,在文件开头(shebang 之后)插入
|
|
1382
|
+
content = content.replace(/(^#!.*\n)?/, "$1\n" + envSetup + "\n");
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
return content;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
/**
|
|
1389
|
+
* 管理当前工作空间的 nginx 实例
|
|
1390
|
+
*/
|
|
1391
|
+
async manageSpaceNginx(options) {
|
|
1392
|
+
const { start, stop, reload, status } = options;
|
|
1393
|
+
|
|
1394
|
+
// 确保配置存在
|
|
1395
|
+
const config = this.loadConfig();
|
|
1396
|
+
|
|
1397
|
+
if (!config.nginx?.installed) {
|
|
1398
|
+
console.log(chalk.yellow("⚠ 当前工作空间尚未初始化 nginx"));
|
|
1399
|
+
console.log(chalk.gray(" 请运行: wsc nginx init"));
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
const nginxDir = path.join(this.workspaceDir, "nginx");
|
|
1404
|
+
const startScript = path.join(nginxDir, "start-nginx.ps1");
|
|
1405
|
+
const stopScript = path.join(nginxDir, "stop-nginx.ps1");
|
|
1406
|
+
const reloadScript = path.join(nginxDir, "reload-nginx.ps1");
|
|
1407
|
+
const statusScript = path.join(nginxDir, "status-nginx.ps1");
|
|
1408
|
+
|
|
1409
|
+
if (start) {
|
|
1410
|
+
console.log(chalk.blue("启动工作空间 nginx..."));
|
|
1411
|
+
if (!fs.existsSync(startScript)) {
|
|
1412
|
+
console.log(chalk.red(`✗ 启动脚本不存在: ${startScript}`));
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
try {
|
|
1417
|
+
// 检测可用端口(从5000开始)
|
|
1418
|
+
console.log(chalk.gray(" 检测可用端口..."));
|
|
1419
|
+
const availablePort = await this.findAvailablePort(5000);
|
|
1420
|
+
console.log(chalk.green(` ✓ 使用端口: ${availablePort}`));
|
|
1421
|
+
|
|
1422
|
+
// 更新配置文件中的端口
|
|
1423
|
+
const sourceDir = this.getNginxConfigDir();
|
|
1424
|
+
const configDir = config.nginx.configDir;
|
|
1425
|
+
if (sourceDir && configDir && fs.existsSync(configDir)) {
|
|
1426
|
+
const proxyTarget = config.nginx.proxyTarget || "http://localhost:8080";
|
|
1427
|
+
const workspaceDir = this.baseDir.replace(/\\/g, "/");
|
|
1428
|
+
|
|
1429
|
+
const templateFiles = fs.readdirSync(sourceDir).filter(f => f.endsWith(".conf"));
|
|
1430
|
+
for (const file of templateFiles) {
|
|
1431
|
+
const srcPath = path.join(sourceDir, file);
|
|
1432
|
+
const dstPath = path.join(configDir, file);
|
|
1433
|
+
|
|
1434
|
+
let content;
|
|
1435
|
+
if (fs.existsSync(dstPath)) {
|
|
1436
|
+
// 配置文件已存在,读取现有配置并更新端口
|
|
1437
|
+
content = fs.readFileSync(dstPath, "utf-8");
|
|
1438
|
+
// 替换 listen 行中的端口号(包括 <serveport> 占位符或具体数字)
|
|
1439
|
+
content = content.replace(/listen\s+\S+;/gi, `listen ${availablePort};`);
|
|
1440
|
+
console.log(chalk.gray(` ✓ 更新端口: ${file} (端口: ${availablePort})`));
|
|
1441
|
+
} else {
|
|
1442
|
+
// 配置文件不存在,从模板复制
|
|
1443
|
+
content = fs.readFileSync(srcPath, "utf-8");
|
|
1444
|
+
content = this.replaceNginxPlaceholders(
|
|
1445
|
+
content,
|
|
1446
|
+
workspaceDir,
|
|
1447
|
+
proxyTarget,
|
|
1448
|
+
availablePort,
|
|
1449
|
+
);
|
|
1450
|
+
console.log(chalk.gray(` ✓ 复制配置: ${file} (端口: ${availablePort})`));
|
|
1451
|
+
}
|
|
1452
|
+
fs.writeFileSync(dstPath, content, "utf-8");
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// 保存端口到配置
|
|
1457
|
+
config.nginx.servePort = availablePort;
|
|
1458
|
+
this.saveConfig(config);
|
|
1459
|
+
|
|
1460
|
+
// 使用 pwsh (PowerShell Core) 执行脚本,支持更好的 UTF-8 编码
|
|
1461
|
+
// 优先使用 pwsh (PowerShell 7),找不到则使用 powershell (Windows PowerShell 5.1)
|
|
1462
|
+
const psCmd = this.findPowerShellCmd();
|
|
1463
|
+
execSync(`${psCmd} -ExecutionPolicy Bypass -File "${startScript}"`, {
|
|
1464
|
+
stdio: "inherit",
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
// 打印访问地址
|
|
1468
|
+
console.log();
|
|
1469
|
+
console.log(chalk.green("========================================"));
|
|
1470
|
+
console.log(chalk.green(" Nginx 启动成功!"));
|
|
1471
|
+
console.log(chalk.green("========================================"));
|
|
1472
|
+
console.log();
|
|
1473
|
+
console.log(chalk.blue(" 访问地址:"));
|
|
1474
|
+
console.log(chalk.cyan(` http://localhost:${availablePort}`));
|
|
1475
|
+
console.log();
|
|
1476
|
+
console.log(chalk.gray(" 代理后端:"));
|
|
1477
|
+
console.log(chalk.gray(` ${config.nginx.proxyTarget || "http://localhost:8080"}`));
|
|
1478
|
+
console.log();
|
|
1479
|
+
} catch (error) {
|
|
1480
|
+
console.log(chalk.red("✗ 启动失败"), error.message);
|
|
1481
|
+
}
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
if (stop) {
|
|
1486
|
+
console.log(chalk.blue("停止工作空间 nginx..."));
|
|
1487
|
+
if (!fs.existsSync(stopScript)) {
|
|
1488
|
+
console.log(chalk.red(`✗ 停止脚本不存在: ${stopScript}`));
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
try {
|
|
1492
|
+
const psCmd = this.findPowerShellCmd();
|
|
1493
|
+
execSync(`${psCmd} -ExecutionPolicy Bypass -File "${stopScript}"`, {
|
|
1494
|
+
stdio: "inherit",
|
|
1495
|
+
});
|
|
1496
|
+
console.log(chalk.green("✓ nginx 已停止"));
|
|
1497
|
+
|
|
1498
|
+
// 清除端口配置
|
|
1499
|
+
if (config.nginx.servePort) {
|
|
1500
|
+
delete config.nginx.servePort;
|
|
1501
|
+
this.saveConfig(config);
|
|
1502
|
+
}
|
|
1503
|
+
} catch (error) {
|
|
1504
|
+
console.log(chalk.yellow("⚠ 停止命令执行失败或 nginx 未运行"));
|
|
1505
|
+
}
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
if (reload) {
|
|
1510
|
+
console.log(chalk.blue("重载工作空间 nginx..."));
|
|
1511
|
+
if (!fs.existsSync(reloadScript)) {
|
|
1512
|
+
console.log(chalk.red(`✗ 重载脚本不存在: ${reloadScript}`));
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1515
|
+
try {
|
|
1516
|
+
const psCmd = this.findPowerShellCmd();
|
|
1517
|
+
execSync(`${psCmd} -ExecutionPolicy Bypass -File "${reloadScript}"`, {
|
|
1518
|
+
stdio: "inherit",
|
|
1519
|
+
});
|
|
1520
|
+
console.log(chalk.green("✓ nginx 配置已重载"));
|
|
1521
|
+
} catch (error) {
|
|
1522
|
+
console.log(chalk.red("✗ 重载失败"), error.message);
|
|
1523
|
+
}
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
if (status) {
|
|
1528
|
+
console.log(chalk.blue("检查工作空间 nginx 状态..."));
|
|
1529
|
+
if (!fs.existsSync(statusScript)) {
|
|
1530
|
+
// 如果没有状态脚本,直接检查进程
|
|
1531
|
+
try {
|
|
1532
|
+
const output = execSync("tasklist | findstr nginx", {
|
|
1533
|
+
stdio: "pipe",
|
|
1534
|
+
encoding: "utf-8",
|
|
1535
|
+
});
|
|
1536
|
+
console.log(chalk.green("✓ nginx 正在运行"));
|
|
1537
|
+
console.log();
|
|
1538
|
+
console.log(output);
|
|
1539
|
+
} catch {
|
|
1540
|
+
console.log(chalk.yellow("⚠ nginx 未运行"));
|
|
1541
|
+
}
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
try {
|
|
1545
|
+
const psCmd = this.findPowerShellCmd();
|
|
1546
|
+
execSync(`${psCmd} -ExecutionPolicy Bypass -File "${statusScript}"`, {
|
|
1547
|
+
stdio: "inherit",
|
|
1548
|
+
});
|
|
1549
|
+
} catch {
|
|
1550
|
+
// 脚本内部处理错误
|
|
1551
|
+
}
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
// 默认显示帮助信息
|
|
1556
|
+
console.log(chalk.blue("\n工作空间 Nginx 管理"));
|
|
1557
|
+
console.log(chalk.gray(` 工作空间: ${this.baseDir}`));
|
|
1558
|
+
console.log(chalk.gray(` 配置目录: ${config.nginx.configDir}`));
|
|
1559
|
+
|
|
1560
|
+
// 显示当前端口配置
|
|
1561
|
+
if (config.nginx.servePort) {
|
|
1562
|
+
console.log(chalk.gray(` 服务端口: ${config.nginx.servePort}`));
|
|
1563
|
+
console.log();
|
|
1564
|
+
console.log(chalk.blue(" 访问地址:"));
|
|
1565
|
+
console.log(chalk.cyan(` http://localhost:${config.nginx.servePort}`));
|
|
1566
|
+
}
|
|
1567
|
+
console.log();
|
|
1568
|
+
|
|
1569
|
+
console.log(chalk.blue("可用命令:"));
|
|
1570
|
+
console.log(chalk.gray(" wsc nginx start # 启动"));
|
|
1571
|
+
console.log(chalk.gray(" wsc nginx stop # 停止"));
|
|
1572
|
+
console.log(chalk.gray(" wsc nginx reload # 重载配置"));
|
|
1573
|
+
console.log(chalk.gray(" wsc nginx status # 查看状态"));
|
|
1574
|
+
console.log();
|
|
1575
|
+
|
|
1576
|
+
// 显示配置列表
|
|
1577
|
+
if (config.nginx.configs?.length > 0) {
|
|
1578
|
+
console.log(chalk.blue("可用配置:"));
|
|
1579
|
+
for (const conf of config.nginx.configs) {
|
|
1580
|
+
console.log(
|
|
1581
|
+
chalk.gray(
|
|
1582
|
+
` • ${conf.name}${conf.port ? ` (端口: ${conf.port})` : ""}`,
|
|
1583
|
+
),
|
|
1584
|
+
);
|
|
1585
|
+
}
|
|
1586
|
+
console.log();
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
module.exports = WorkspaceManager;
|