zen-gitsync 2.14.3 → 2.15.1
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/package.json +1 -1
- package/src/ui/public/assets/{AppVersionBadge-z2kqhmtD.js → AppVersionBadge-BoAhI0Kt.js} +2 -2
- package/src/ui/public/assets/BranchSelector--8Qfb75A.js +1 -0
- package/src/ui/public/assets/CommitForm-CobjwXah.js +6 -0
- package/src/ui/public/assets/ConsoleView-B2WZRKsP.css +1 -0
- package/src/ui/public/assets/ConsoleView-HcfiYLnM.js +17 -0
- package/src/ui/public/assets/CustomCommandManager-DQG4wRU7.js +1 -0
- package/src/ui/public/assets/{EditorView-aoA30emq.js → EditorView-CAjuz7yY.js} +0 -0
- package/src/ui/public/assets/{FlowExecutionViewer-BvXlaQai.js → FlowExecutionViewer-C8H9uoZL.js} +1 -1
- package/src/ui/public/assets/FlowOrchestrationWorkspace-C2RMFB-p.js +21 -0
- package/src/ui/public/assets/{LogList-uF4tir23.js → LogList-8qSNrgT7.js} +4 -4
- package/src/ui/public/assets/{MindmapView-B8wr0EFL.js → MindmapView-C5pLBOs_.js} +1 -1
- package/src/ui/public/assets/{MonitorView-DaJ0h8Im.js → MonitorView-Bijvi8pi.js} +1 -1
- package/src/ui/public/assets/ProjectStartupButton-Cb-RAZqA.js +1 -0
- package/src/ui/public/assets/{RemoteRepoCard-BDbJ76uf.js → RemoteRepoCard-DjfI188H.js} +1 -1
- package/src/ui/public/assets/{SourceMapView-2HjAlHnp.js → SourceMapView-DbdbojUR.js} +2 -2
- package/src/ui/public/assets/{SvgIcon-CK6VGHb5.js → SvgIcon-BVBpj6nK.js} +1 -1
- package/src/ui/public/assets/TemplateManager-C0rm8xfC.css +1 -0
- package/src/ui/public/assets/TemplateManager-CSI1wQZY.js +1 -0
- package/src/ui/public/assets/{UserInputNode-Cx8r5geA.js → UserInputNode-8ocSzj-6.js} +1 -1
- package/src/ui/public/assets/{WorkbenchView-CG54iND-.js → WorkbenchView-DAXAzJTK.js} +1 -1
- package/src/ui/public/assets/{_plugin-vue_export-helper-CJMndG_W.js → _plugin-vue_export-helper-Uwc4Esmp.js} +4 -4
- package/src/ui/public/assets/commandParser-wwLxoJ92.js +1 -0
- package/src/ui/public/assets/{configStore-DDnE5eie.js → configStore-BHFJlgWp.js} +1 -1
- package/src/ui/public/assets/element-plus-R1nMzM9j.css +1 -0
- package/src/ui/public/assets/{flow-mindmap-CAkGY2ir.js → flow-mindmap-C0IUPqjN.js} +1 -1
- package/src/ui/public/assets/index-BZk-ldXr.css +1 -0
- package/src/ui/public/assets/index-Bl8fyL3X.js +23 -0
- package/src/ui/public/assets/{vendor-BuiZXcjj.js → vendor-0hkrGFDb.js} +1 -1
- package/src/ui/public/assets/{vue-flow-C6RXt-U3.js → vue-flow-CkwNeuGO.js} +1 -1
- package/src/ui/public/index.html +9 -11
- package/src/ui/server/routes/branchStatus.test.js +37 -0
- package/src/ui/server/routes/npm.js +1387 -1327
- package/src/utils/index.js +10 -3
- package/src/ui/public/assets/BranchSelector-CSjsCeeJ.js +0 -1
- package/src/ui/public/assets/CommandConsole-96MUoG1F.js +0 -17
- package/src/ui/public/assets/CommandConsole-Da3j09oc.css +0 -1
- package/src/ui/public/assets/CommitForm-BBKQ0GqO.js +0 -6
- package/src/ui/public/assets/CommonDialog-CgQovpCJ.js +0 -1
- package/src/ui/public/assets/CommonDialog-Cyh3xu0p.css +0 -1
- package/src/ui/public/assets/FlowOrchestrationWorkspace-CgWUF1Jc.js +0 -21
- package/src/ui/public/assets/ProjectStartupButton-CcH1NL2W.js +0 -1
- package/src/ui/public/assets/element-plus-D06ZgZjR.css +0 -1
- package/src/ui/public/assets/index-B7FGNx_l.js +0 -23
- package/src/ui/public/assets/index-f6UhuE5j.css +0 -1
- /package/src/ui/public/assets/{element-plus-B6Owr5jY.js → element-plus-tbZnDtyd.js} +0 -0
|
@@ -1,979 +1,1039 @@
|
|
|
1
|
-
// Copyright 2026 xz333221
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
//
|
|
15
|
-
import express from 'express';
|
|
16
|
-
import logger from '../utils/logger.js'
|
|
17
|
-
import fs from 'fs/promises';
|
|
18
|
-
import fsSync from 'fs';
|
|
19
|
-
import path from 'path';
|
|
20
|
-
import os from 'os';
|
|
21
|
-
import { exec, spawn } from 'child_process';
|
|
22
|
-
import https from 'https';
|
|
23
|
-
import { fileURLToPath } from 'url';
|
|
24
|
-
import { getRegistryPath } from '../utils/instanceRegistry.js';
|
|
25
|
-
|
|
26
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
27
|
-
const __dirname = path.dirname(__filename);
|
|
28
|
-
|
|
29
|
-
export function registerNpmRoutes({
|
|
30
|
-
app,
|
|
31
|
-
getCurrentProjectPath
|
|
32
|
-
}) {
|
|
33
|
-
// 标记是否有升级任务在跑(防并发)
|
|
34
|
-
let activeUpgrade = false;
|
|
35
|
-
// 标记是否有重启任务在跑(防并发)
|
|
36
|
-
let activeRestart = false;
|
|
37
|
-
// 自拉起重启相关常量
|
|
38
|
-
const RESTART_TIMEOUT_MS =
|
|
39
|
-
const REGISTRY_POLL_MS = 250; // 轮询注册表文件间隔
|
|
40
|
-
const EXIT_DELAY_MS = 100; // 子进程就绪后给前端的最后缓冲时间
|
|
41
|
-
|
|
42
|
-
// ========== 应用自升级相关 API ==========
|
|
43
|
-
|
|
44
|
-
// 解析"自拉起"要 spawn 的入口绝对路径。
|
|
45
|
-
// 优先 process.argv[1](开发态 server.js / 生产态 src/gitCommit.js 都是绝对路径),
|
|
46
|
-
// 但 launch.json 的 backend 是 `node -e "...process.argv = [...]; require('./server.js')"`,
|
|
47
|
-
// 这种篡改出来的 argv[1] 是相对路径 "server.js",不能直接 spawn。
|
|
48
|
-
// 回退: 用 __dirname 推导 src/ui/server/index.js 的同级 server.js(仓库根)
|
|
49
|
-
function resolveRestartEntry() {
|
|
50
|
-
const arg = process.argv[1]
|
|
51
|
-
if (arg && path.isAbsolute(arg) && fsSync.existsSync(arg)) {
|
|
52
|
-
return { entry: arg, args: process.argv.slice(2) }
|
|
53
|
-
}
|
|
54
|
-
// 回退到仓库根的 server.js(开发态入口),保留原 PORT 等环境变量
|
|
55
|
-
const fallback = path.resolve(__dirname, '../../../..', 'server.js')
|
|
56
|
-
if (fsSync.existsSync(fallback)) {
|
|
57
|
-
// 透传用户原始 argv[2..](--no-open 等);如果原 argv[1] 是 "server.js" 这类相对路径,
|
|
58
|
-
// 用户大概率没传任何 args,所以空数组也合理
|
|
59
|
-
const originalArgs = (arg && !path.isAbsolute(arg)) ? process.argv.slice(2) : []
|
|
60
|
-
return { entry: fallback, args: originalArgs }
|
|
61
|
-
}
|
|
62
|
-
return null
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// 当前安装的版本(从外层 package.json 读取)
|
|
66
|
-
function getCurrentVersion() {
|
|
67
|
-
try {
|
|
68
|
-
// src/ui/server/routes/npm.js -> ../../../../package.json
|
|
69
|
-
const pkgPath = path.resolve(__dirname, '../../../..', 'package.json')
|
|
70
|
-
if (fsSync.existsSync(pkgPath)) {
|
|
71
|
-
const pkg = JSON.parse(fsSync.readFileSync(pkgPath, 'utf8'))
|
|
72
|
-
return pkg.version || '0.0.0'
|
|
73
|
-
}
|
|
74
|
-
} catch (err) {
|
|
75
|
-
logger.error('读取本地 package.json 版本失败:', err)
|
|
76
|
-
}
|
|
77
|
-
return '0.0.0'
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// 简单的 semver 比较:a > b 返回 1, a < b 返回 -1, 相等返回 0
|
|
81
|
-
function compareSemver(a, b) {
|
|
82
|
-
const pa = a.replace(/^v/, '').split('.').map(n => parseInt(n, 10) || 0)
|
|
83
|
-
const pb = b.replace(/^v/, '').split('.').map(n => parseInt(n, 10) || 0)
|
|
84
|
-
for (let i = 0; i < 3; i++) {
|
|
85
|
-
if ((pa[i] || 0) > (pb[i] || 0)) return 1
|
|
86
|
-
if ((pa[i] || 0) < (pb[i] || 0)) return -1
|
|
87
|
-
}
|
|
88
|
-
return 0
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// 缓存 npm registry latest 结果 10 分钟
|
|
92
|
-
let latestVersionCache = { value: null, at: 0 }
|
|
93
|
-
const LATEST_TTL_MS = 10 * 60 * 1000
|
|
94
|
-
|
|
95
|
-
function fetchLatestFromRegistry() {
|
|
96
|
-
return new Promise((resolve, reject) => {
|
|
97
|
-
const req = https.get(
|
|
98
|
-
'https://registry.npmjs.org/zen-gitsync/latest',
|
|
99
|
-
{ timeout: 5000, headers: { 'User-Agent': 'zen-gitsync-app' } },
|
|
100
|
-
(res) => {
|
|
101
|
-
let data = ''
|
|
102
|
-
res.on('data', chunk => { data += chunk })
|
|
103
|
-
res.on('end', () => {
|
|
104
|
-
if (res.statusCode !== 200) {
|
|
105
|
-
return reject(new Error(`registry 返回状态 ${res.statusCode}`))
|
|
106
|
-
}
|
|
107
|
-
try {
|
|
108
|
-
const pkg = JSON.parse(data)
|
|
109
|
-
resolve(pkg.version || null)
|
|
110
|
-
} catch (e) {
|
|
111
|
-
reject(new Error('解析 registry 响应失败'))
|
|
112
|
-
}
|
|
113
|
-
})
|
|
114
|
-
}
|
|
115
|
-
)
|
|
116
|
-
req.on('timeout', () => {
|
|
117
|
-
req.destroy(new Error('registry 请求超时'))
|
|
118
|
-
})
|
|
119
|
-
req.on('error', err => reject(err))
|
|
120
|
-
})
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async function getLatestVersion() {
|
|
124
|
-
const now = Date.now()
|
|
125
|
-
if (latestVersionCache.value && now - latestVersionCache.at < LATEST_TTL_MS) {
|
|
126
|
-
return latestVersionCache.value
|
|
127
|
-
}
|
|
128
|
-
const v = await fetchLatestFromRegistry()
|
|
129
|
-
latestVersionCache = { value: v, at: now }
|
|
130
|
-
return v
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// GET /api/app-version
|
|
134
|
-
app.get('/api/app-version', async (_req, res) => {
|
|
135
|
-
const current = getCurrentVersion()
|
|
136
|
-
try {
|
|
137
|
-
const latest = await getLatestVersion()
|
|
138
|
-
const hasUpdate = !!latest && compareSemver(latest, current) > 0
|
|
139
|
-
res.json({ success: true, current, latest, hasUpdate })
|
|
140
|
-
} catch (err) {
|
|
141
|
-
// 拉取失败时仍返回当前版本,前端据此决定是否显示升级按钮
|
|
142
|
-
res.json({
|
|
143
|
-
success: false,
|
|
144
|
-
current,
|
|
145
|
-
latest: null,
|
|
146
|
-
hasUpdate: false,
|
|
147
|
-
error: err.message
|
|
148
|
-
})
|
|
149
|
-
}
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
// POST /api/app-upgrade - 流式 NDJSON: { type: 'stdout'|'stderr'|'done'|'error', ... }
|
|
153
|
-
app.post('/api/app-upgrade', (req, res) => {
|
|
154
|
-
// 防并发:同一时刻只允许一个升级任务
|
|
155
|
-
if (activeUpgrade) {
|
|
156
|
-
res.status(409).json({ success: false, error: '已有升级任务在进行中' })
|
|
157
|
-
return
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
res.setHeader('Content-Type', 'application/x-ndjson; charset=utf-8')
|
|
161
|
-
res.setHeader('Cache-Control', 'no-cache')
|
|
162
|
-
res.setHeader('Connection', 'keep-alive')
|
|
163
|
-
res.flushHeaders?.()
|
|
164
|
-
|
|
165
|
-
const send = (obj) => {
|
|
166
|
-
res.write(JSON.stringify(obj) + '\n')
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Windows 上 npm install -g 通常不需要管理员;*nix 需要 sudo。
|
|
170
|
-
// 用 sudo -n(非交互式)检测是否可免密,失败时让用户用 sudo 重启 GUI
|
|
171
|
-
const isWin = process.platform === 'win32'
|
|
172
|
-
const npmCmd = isWin ? 'npm.cmd' : 'npm'
|
|
173
|
-
const args = ['install', '-g', 'zen-gitsync', '--registry', 'https://registry.npmjs.org/']
|
|
174
|
-
const cmd = isWin ? npmCmd : 'sudo'
|
|
175
|
-
const finalArgs = isWin ? args : ['-n', npmCmd, ...args]
|
|
176
|
-
|
|
177
|
-
activeUpgrade = send
|
|
178
|
-
const cleanup = () => { activeUpgrade = null }
|
|
179
|
-
|
|
180
|
-
let child
|
|
181
|
-
try {
|
|
182
|
-
child = spawn(cmd, finalArgs, {
|
|
183
|
-
env: { ...process.env, FORCE_COLOR: '0' },
|
|
184
|
-
windowsHide: true,
|
|
185
|
-
// Windows 下调用 .cmd 必须 shell:true
|
|
186
|
-
shell: isWin
|
|
187
|
-
})
|
|
188
|
-
} catch (err) {
|
|
189
|
-
send({ type: 'error', message: `启动升级进程失败: ${err.message}` })
|
|
190
|
-
send({ type: 'done', code: -1 })
|
|
191
|
-
res.end()
|
|
192
|
-
cleanup()
|
|
193
|
-
return
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
send({ type: 'stdout', message: `$ ${cmd} ${finalArgs.join(' ')}\n` })
|
|
197
|
-
|
|
198
|
-
child.stdout.on('data', (buf) => send({ type: 'stdout', message: buf.toString() }))
|
|
199
|
-
child.stderr.on('data', (buf) => send({ type: 'stderr', message: buf.toString() }))
|
|
200
|
-
|
|
201
|
-
child.on('error', (err) => {
|
|
202
|
-
send({ type: 'error', message: `升级进程错误: ${err.message}` })
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
child.on('close', (code) => {
|
|
206
|
-
if (code === 0) {
|
|
207
|
-
send({ type: 'stdout', message: '\n✅ 升级完成,新版本已全局安装。\n' })
|
|
208
|
-
// 清理缓存,强制下次重新拉 latest
|
|
209
|
-
latestVersionCache = { value: null, at: 0 }
|
|
210
|
-
} else {
|
|
211
|
-
let hint = ''
|
|
212
|
-
if (!isWin) {
|
|
213
|
-
hint = '(提示:在 macOS/Linux 上全局安装需要 sudo,请用管理员权限重启 GUI 后再试)'
|
|
214
|
-
}
|
|
215
|
-
send({ type: 'error', message: `\n升级失败,退出码 ${code}${hint ? ' ' + hint : ''}\n` })
|
|
216
|
-
}
|
|
217
|
-
send({ type: 'done', code })
|
|
218
|
-
res.end()
|
|
219
|
-
cleanup()
|
|
220
|
-
})
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
// 同步读取 instanceRegistry 文件并返回 Map<pid, entry>
|
|
225
|
-
// npm.js 在自拉起重启场景下需要轮询此文件判断子进程是否已绑定端口
|
|
226
|
-
function readRegistrySync(p) {
|
|
227
|
-
const m = new Map();
|
|
228
|
-
try {
|
|
229
|
-
const raw = fsSync.readFileSync(p, 'utf-8');
|
|
230
|
-
const obj = JSON.parse(raw);
|
|
231
|
-
const instances = (obj && obj.instances) || {};
|
|
232
|
-
for (const [pidStr, entry] of Object.entries(instances)) {
|
|
233
|
-
const pid = Number(pidStr);
|
|
234
|
-
if (Number.isFinite(pid) && entry && typeof entry === 'object') {
|
|
235
|
-
m.set(pid, entry);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
} catch (_) {
|
|
239
|
-
// 文件不存在或 JSON 损坏 → 返回空 Map,调用方按"还没注册"处理
|
|
240
|
-
}
|
|
241
|
-
return m;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// POST /api/app-restart - 自拉起重启:spawn 当前进程入口 + 轮询 instanceRegistry
|
|
245
|
-
// 拿到子进程端口后通过 NDJSON 流告知前端;旧进程再退出
|
|
246
|
-
app.post('/api/app-restart', (_req, res) => {
|
|
247
|
-
if (activeRestart) {
|
|
248
|
-
res.status(409).json({ success: false, error: '已有重启任务在进行中' });
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
activeRestart = true;
|
|
252
|
-
|
|
253
|
-
res.setHeader('Content-Type', 'application/x-ndjson; charset=utf-8');
|
|
254
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
255
|
-
res.setHeader('Connection', 'keep-alive');
|
|
256
|
-
res.flushHeaders?.();
|
|
257
|
-
|
|
258
|
-
const send = (obj) => { try { res.write(JSON.stringify(obj) + '\n') } catch (_) {} };
|
|
259
|
-
const finish = () => { try { res.end() } catch (_) {} };
|
|
260
|
-
const release = () => { activeRestart = false };
|
|
261
|
-
|
|
262
|
-
// 1. 记录当前所有 instance,后续用"出现的新 pid"判定子进程
|
|
263
|
-
const registryPath = getRegistryPath();
|
|
264
|
-
const before = readRegistrySync(registryPath);
|
|
265
|
-
const start = Date.now();
|
|
266
|
-
const spawnAt = Date.now();
|
|
267
|
-
|
|
268
|
-
// 2. 准备子进程 ready 信号文件 —— startServerOnAvailablePort 在 listen 成功后会写入
|
|
269
|
-
// (instanceRegistry 在 EPERM 环境下不可用,这是回退通道)
|
|
270
|
-
const notifyPath = path.join(
|
|
271
|
-
os.tmpdir(),
|
|
272
|
-
`zen-gitsync-restart-${process.pid}-${Date.now()}.port`
|
|
273
|
-
);
|
|
274
|
-
try { fsSync.unlinkSync(notifyPath) } catch (_) {} // 清掉旧同名文件
|
|
275
|
-
|
|
276
|
-
const entryInfo = resolveRestartEntry();
|
|
277
|
-
if (!entryInfo) {
|
|
278
|
-
send({ type: 'error', message: '无法解析重启入口(process.argv[1] 既不是绝对路径,仓库根也没有 server.js)' });
|
|
279
|
-
finish();
|
|
280
|
-
release();
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
} catch (
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
//
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
'
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
'
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
//
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
depthStats[depth].
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
const
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
'
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1
|
+
// Copyright 2026 xz333221
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
//
|
|
15
|
+
import express from 'express';
|
|
16
|
+
import logger from '../utils/logger.js'
|
|
17
|
+
import fs from 'fs/promises';
|
|
18
|
+
import fsSync from 'fs';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import os from 'os';
|
|
21
|
+
import { exec, spawn } from 'child_process';
|
|
22
|
+
import https from 'https';
|
|
23
|
+
import { fileURLToPath } from 'url';
|
|
24
|
+
import { getRegistryPath } from '../utils/instanceRegistry.js';
|
|
25
|
+
|
|
26
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
27
|
+
const __dirname = path.dirname(__filename);
|
|
28
|
+
|
|
29
|
+
export function registerNpmRoutes({
|
|
30
|
+
app,
|
|
31
|
+
getCurrentProjectPath
|
|
32
|
+
}) {
|
|
33
|
+
// 标记是否有升级任务在跑(防并发)
|
|
34
|
+
let activeUpgrade = false;
|
|
35
|
+
// 标记是否有重启任务在跑(防并发)
|
|
36
|
+
let activeRestart = false;
|
|
37
|
+
// 自拉起重启相关常量
|
|
38
|
+
const RESTART_TIMEOUT_MS = 15000; // 子进程必须在此时间内注册到 instanceRegistry(Windows 冷启动偏慢)
|
|
39
|
+
const REGISTRY_POLL_MS = 250; // 轮询注册表文件间隔
|
|
40
|
+
const EXIT_DELAY_MS = 100; // 子进程就绪后给前端的最后缓冲时间
|
|
41
|
+
|
|
42
|
+
// ========== 应用自升级相关 API ==========
|
|
43
|
+
|
|
44
|
+
// 解析"自拉起"要 spawn 的入口绝对路径。
|
|
45
|
+
// 优先 process.argv[1](开发态 server.js / 生产态 src/gitCommit.js 都是绝对路径),
|
|
46
|
+
// 但 launch.json 的 backend 是 `node -e "...process.argv = [...]; require('./server.js')"`,
|
|
47
|
+
// 这种篡改出来的 argv[1] 是相对路径 "server.js",不能直接 spawn。
|
|
48
|
+
// 回退: 用 __dirname 推导 src/ui/server/index.js 的同级 server.js(仓库根)
|
|
49
|
+
function resolveRestartEntry() {
|
|
50
|
+
const arg = process.argv[1]
|
|
51
|
+
if (arg && path.isAbsolute(arg) && fsSync.existsSync(arg)) {
|
|
52
|
+
return { entry: arg, args: process.argv.slice(2) }
|
|
53
|
+
}
|
|
54
|
+
// 回退到仓库根的 server.js(开发态入口),保留原 PORT 等环境变量
|
|
55
|
+
const fallback = path.resolve(__dirname, '../../../..', 'server.js')
|
|
56
|
+
if (fsSync.existsSync(fallback)) {
|
|
57
|
+
// 透传用户原始 argv[2..](--no-open 等);如果原 argv[1] 是 "server.js" 这类相对路径,
|
|
58
|
+
// 用户大概率没传任何 args,所以空数组也合理
|
|
59
|
+
const originalArgs = (arg && !path.isAbsolute(arg)) ? process.argv.slice(2) : []
|
|
60
|
+
return { entry: fallback, args: originalArgs }
|
|
61
|
+
}
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 当前安装的版本(从外层 package.json 读取)
|
|
66
|
+
function getCurrentVersion() {
|
|
67
|
+
try {
|
|
68
|
+
// src/ui/server/routes/npm.js -> ../../../../package.json
|
|
69
|
+
const pkgPath = path.resolve(__dirname, '../../../..', 'package.json')
|
|
70
|
+
if (fsSync.existsSync(pkgPath)) {
|
|
71
|
+
const pkg = JSON.parse(fsSync.readFileSync(pkgPath, 'utf8'))
|
|
72
|
+
return pkg.version || '0.0.0'
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
logger.error('读取本地 package.json 版本失败:', err)
|
|
76
|
+
}
|
|
77
|
+
return '0.0.0'
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 简单的 semver 比较:a > b 返回 1, a < b 返回 -1, 相等返回 0
|
|
81
|
+
function compareSemver(a, b) {
|
|
82
|
+
const pa = a.replace(/^v/, '').split('.').map(n => parseInt(n, 10) || 0)
|
|
83
|
+
const pb = b.replace(/^v/, '').split('.').map(n => parseInt(n, 10) || 0)
|
|
84
|
+
for (let i = 0; i < 3; i++) {
|
|
85
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return 1
|
|
86
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return -1
|
|
87
|
+
}
|
|
88
|
+
return 0
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 缓存 npm registry latest 结果 10 分钟
|
|
92
|
+
let latestVersionCache = { value: null, at: 0 }
|
|
93
|
+
const LATEST_TTL_MS = 10 * 60 * 1000
|
|
94
|
+
|
|
95
|
+
function fetchLatestFromRegistry() {
|
|
96
|
+
return new Promise((resolve, reject) => {
|
|
97
|
+
const req = https.get(
|
|
98
|
+
'https://registry.npmjs.org/zen-gitsync/latest',
|
|
99
|
+
{ timeout: 5000, headers: { 'User-Agent': 'zen-gitsync-app' } },
|
|
100
|
+
(res) => {
|
|
101
|
+
let data = ''
|
|
102
|
+
res.on('data', chunk => { data += chunk })
|
|
103
|
+
res.on('end', () => {
|
|
104
|
+
if (res.statusCode !== 200) {
|
|
105
|
+
return reject(new Error(`registry 返回状态 ${res.statusCode}`))
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const pkg = JSON.parse(data)
|
|
109
|
+
resolve(pkg.version || null)
|
|
110
|
+
} catch (e) {
|
|
111
|
+
reject(new Error('解析 registry 响应失败'))
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
req.on('timeout', () => {
|
|
117
|
+
req.destroy(new Error('registry 请求超时'))
|
|
118
|
+
})
|
|
119
|
+
req.on('error', err => reject(err))
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function getLatestVersion() {
|
|
124
|
+
const now = Date.now()
|
|
125
|
+
if (latestVersionCache.value && now - latestVersionCache.at < LATEST_TTL_MS) {
|
|
126
|
+
return latestVersionCache.value
|
|
127
|
+
}
|
|
128
|
+
const v = await fetchLatestFromRegistry()
|
|
129
|
+
latestVersionCache = { value: v, at: now }
|
|
130
|
+
return v
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// GET /api/app-version
|
|
134
|
+
app.get('/api/app-version', async (_req, res) => {
|
|
135
|
+
const current = getCurrentVersion()
|
|
136
|
+
try {
|
|
137
|
+
const latest = await getLatestVersion()
|
|
138
|
+
const hasUpdate = !!latest && compareSemver(latest, current) > 0
|
|
139
|
+
res.json({ success: true, current, latest, hasUpdate })
|
|
140
|
+
} catch (err) {
|
|
141
|
+
// 拉取失败时仍返回当前版本,前端据此决定是否显示升级按钮
|
|
142
|
+
res.json({
|
|
143
|
+
success: false,
|
|
144
|
+
current,
|
|
145
|
+
latest: null,
|
|
146
|
+
hasUpdate: false,
|
|
147
|
+
error: err.message
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
// POST /api/app-upgrade - 流式 NDJSON: { type: 'stdout'|'stderr'|'done'|'error', ... }
|
|
153
|
+
app.post('/api/app-upgrade', (req, res) => {
|
|
154
|
+
// 防并发:同一时刻只允许一个升级任务
|
|
155
|
+
if (activeUpgrade) {
|
|
156
|
+
res.status(409).json({ success: false, error: '已有升级任务在进行中' })
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
res.setHeader('Content-Type', 'application/x-ndjson; charset=utf-8')
|
|
161
|
+
res.setHeader('Cache-Control', 'no-cache')
|
|
162
|
+
res.setHeader('Connection', 'keep-alive')
|
|
163
|
+
res.flushHeaders?.()
|
|
164
|
+
|
|
165
|
+
const send = (obj) => {
|
|
166
|
+
res.write(JSON.stringify(obj) + '\n')
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Windows 上 npm install -g 通常不需要管理员;*nix 需要 sudo。
|
|
170
|
+
// 用 sudo -n(非交互式)检测是否可免密,失败时让用户用 sudo 重启 GUI
|
|
171
|
+
const isWin = process.platform === 'win32'
|
|
172
|
+
const npmCmd = isWin ? 'npm.cmd' : 'npm'
|
|
173
|
+
const args = ['install', '-g', 'zen-gitsync', '--registry', 'https://registry.npmjs.org/']
|
|
174
|
+
const cmd = isWin ? npmCmd : 'sudo'
|
|
175
|
+
const finalArgs = isWin ? args : ['-n', npmCmd, ...args]
|
|
176
|
+
|
|
177
|
+
activeUpgrade = send
|
|
178
|
+
const cleanup = () => { activeUpgrade = null }
|
|
179
|
+
|
|
180
|
+
let child
|
|
181
|
+
try {
|
|
182
|
+
child = spawn(cmd, finalArgs, {
|
|
183
|
+
env: { ...process.env, FORCE_COLOR: '0' },
|
|
184
|
+
windowsHide: true,
|
|
185
|
+
// Windows 下调用 .cmd 必须 shell:true
|
|
186
|
+
shell: isWin
|
|
187
|
+
})
|
|
188
|
+
} catch (err) {
|
|
189
|
+
send({ type: 'error', message: `启动升级进程失败: ${err.message}` })
|
|
190
|
+
send({ type: 'done', code: -1 })
|
|
191
|
+
res.end()
|
|
192
|
+
cleanup()
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
send({ type: 'stdout', message: `$ ${cmd} ${finalArgs.join(' ')}\n` })
|
|
197
|
+
|
|
198
|
+
child.stdout.on('data', (buf) => send({ type: 'stdout', message: buf.toString() }))
|
|
199
|
+
child.stderr.on('data', (buf) => send({ type: 'stderr', message: buf.toString() }))
|
|
200
|
+
|
|
201
|
+
child.on('error', (err) => {
|
|
202
|
+
send({ type: 'error', message: `升级进程错误: ${err.message}` })
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
child.on('close', (code) => {
|
|
206
|
+
if (code === 0) {
|
|
207
|
+
send({ type: 'stdout', message: '\n✅ 升级完成,新版本已全局安装。\n' })
|
|
208
|
+
// 清理缓存,强制下次重新拉 latest
|
|
209
|
+
latestVersionCache = { value: null, at: 0 }
|
|
210
|
+
} else {
|
|
211
|
+
let hint = ''
|
|
212
|
+
if (!isWin) {
|
|
213
|
+
hint = '(提示:在 macOS/Linux 上全局安装需要 sudo,请用管理员权限重启 GUI 后再试)'
|
|
214
|
+
}
|
|
215
|
+
send({ type: 'error', message: `\n升级失败,退出码 ${code}${hint ? ' ' + hint : ''}\n` })
|
|
216
|
+
}
|
|
217
|
+
send({ type: 'done', code })
|
|
218
|
+
res.end()
|
|
219
|
+
cleanup()
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
// 同步读取 instanceRegistry 文件并返回 Map<pid, entry>
|
|
225
|
+
// npm.js 在自拉起重启场景下需要轮询此文件判断子进程是否已绑定端口
|
|
226
|
+
function readRegistrySync(p) {
|
|
227
|
+
const m = new Map();
|
|
228
|
+
try {
|
|
229
|
+
const raw = fsSync.readFileSync(p, 'utf-8');
|
|
230
|
+
const obj = JSON.parse(raw);
|
|
231
|
+
const instances = (obj && obj.instances) || {};
|
|
232
|
+
for (const [pidStr, entry] of Object.entries(instances)) {
|
|
233
|
+
const pid = Number(pidStr);
|
|
234
|
+
if (Number.isFinite(pid) && entry && typeof entry === 'object') {
|
|
235
|
+
m.set(pid, entry);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} catch (_) {
|
|
239
|
+
// 文件不存在或 JSON 损坏 → 返回空 Map,调用方按"还没注册"处理
|
|
240
|
+
}
|
|
241
|
+
return m;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// POST /api/app-restart - 自拉起重启:spawn 当前进程入口 + 轮询 instanceRegistry
|
|
245
|
+
// 拿到子进程端口后通过 NDJSON 流告知前端;旧进程再退出
|
|
246
|
+
app.post('/api/app-restart', (_req, res) => {
|
|
247
|
+
if (activeRestart) {
|
|
248
|
+
res.status(409).json({ success: false, error: '已有重启任务在进行中' });
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
activeRestart = true;
|
|
252
|
+
|
|
253
|
+
res.setHeader('Content-Type', 'application/x-ndjson; charset=utf-8');
|
|
254
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
255
|
+
res.setHeader('Connection', 'keep-alive');
|
|
256
|
+
res.flushHeaders?.();
|
|
257
|
+
|
|
258
|
+
const send = (obj) => { try { res.write(JSON.stringify(obj) + '\n') } catch (_) {} };
|
|
259
|
+
const finish = () => { try { res.end() } catch (_) {} };
|
|
260
|
+
const release = () => { activeRestart = false };
|
|
261
|
+
|
|
262
|
+
// 1. 记录当前所有 instance,后续用"出现的新 pid"判定子进程
|
|
263
|
+
const registryPath = getRegistryPath();
|
|
264
|
+
const before = readRegistrySync(registryPath);
|
|
265
|
+
const start = Date.now();
|
|
266
|
+
const spawnAt = Date.now();
|
|
267
|
+
|
|
268
|
+
// 2. 准备子进程 ready 信号文件 —— startServerOnAvailablePort 在 listen 成功后会写入
|
|
269
|
+
// (instanceRegistry 在 EPERM 环境下不可用,这是回退通道)
|
|
270
|
+
const notifyPath = path.join(
|
|
271
|
+
os.tmpdir(),
|
|
272
|
+
`zen-gitsync-restart-${process.pid}-${Date.now()}.port`
|
|
273
|
+
);
|
|
274
|
+
try { fsSync.unlinkSync(notifyPath) } catch (_) {} // 清掉旧同名文件
|
|
275
|
+
|
|
276
|
+
const entryInfo = resolveRestartEntry();
|
|
277
|
+
if (!entryInfo) {
|
|
278
|
+
send({ type: 'error', message: '无法解析重启入口(process.argv[1] 既不是绝对路径,仓库根也没有 server.js)' });
|
|
279
|
+
finish();
|
|
280
|
+
release();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// 子进程 stdout/stderr 写到临时日志文件,便于诊断"cmd 闪退"
|
|
285
|
+
const childLogFile = path.join(
|
|
286
|
+
os.tmpdir(),
|
|
287
|
+
`zen-gitsync-restart-${process.pid}-${Date.now()}.log`
|
|
288
|
+
);
|
|
289
|
+
let childLogFd;
|
|
290
|
+
try {
|
|
291
|
+
childLogFd = fsSync.openSync(childLogFile, 'a');
|
|
292
|
+
} catch (err) {
|
|
293
|
+
send({ type: 'error', message: `打开子进程日志文件失败: ${err?.message || err}` });
|
|
294
|
+
finish();
|
|
295
|
+
release();
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
let child;
|
|
300
|
+
try {
|
|
301
|
+
// spawn 当前进程当时是怎么被 node 启的同一个入口(开发态 server.js / 生产态 gitCommit.js)
|
|
302
|
+
// detached: true + stdio 指向日志文件 + child.unref() → Windows 上父进程 exit 不会带走子进程
|
|
303
|
+
child = spawn(process.execPath, [entryInfo.entry, ...entryInfo.args], {
|
|
304
|
+
detached: true,
|
|
305
|
+
stdio: ['ignore', childLogFd, childLogFd],
|
|
306
|
+
windowsHide: true,
|
|
307
|
+
env: { ...process.env, ZEN_RESTART_NOTIFY_PATH: notifyPath }
|
|
308
|
+
});
|
|
309
|
+
child.unref();
|
|
310
|
+
} catch (err) {
|
|
311
|
+
send({ type: 'error', message: `拉起子进程失败: ${err?.message || err}` });
|
|
312
|
+
try { fsSync.closeSync(childLogFd) } catch (_) {}
|
|
313
|
+
try { fsSync.unlinkSync(notifyPath) } catch (_) {}
|
|
314
|
+
try { fsSync.unlinkSync(childLogFile) } catch (_) {}
|
|
315
|
+
finish();
|
|
316
|
+
release();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
send({ type: 'stdout', message: `[restart] 已在 PID ${child.pid} 拉起子进程 (${entryInfo.entry}),等待端口就绪…\n` });
|
|
321
|
+
send({ type: 'stdout', message: `[restart] 子进程日志: ${childLogFile}\n` });
|
|
322
|
+
|
|
323
|
+
// 监听子进程异常退出,避免"cmd 闪一下没了却没人知道为什么"
|
|
324
|
+
let childDead = false;
|
|
325
|
+
child.on('error', (err) => {
|
|
326
|
+
if (childDead) return;
|
|
327
|
+
childDead = true;
|
|
328
|
+
send({ type: 'error', message: `子进程 spawn 失败: ${err?.message || err}` });
|
|
329
|
+
try { fsSync.closeSync(childLogFd) } catch (_) {}
|
|
330
|
+
try { fsSync.unlinkSync(notifyPath) } catch (_) {}
|
|
331
|
+
finish();
|
|
332
|
+
release();
|
|
333
|
+
});
|
|
334
|
+
child.on('exit', (code, signal) => {
|
|
335
|
+
if (childDead) return;
|
|
336
|
+
childDead = true;
|
|
337
|
+
// 读子进程日志尾部,把崩溃原因推给前端
|
|
338
|
+
let tail = '';
|
|
339
|
+
try {
|
|
340
|
+
const buf = fsSync.readFileSync(childLogFile, 'utf8');
|
|
341
|
+
tail = buf.slice(-2000);
|
|
342
|
+
} catch (_) {}
|
|
343
|
+
send({
|
|
344
|
+
type: 'error',
|
|
345
|
+
message: `子进程提前退出 (code=${code} signal=${signal})${tail ? `\n--- 子进程日志尾部 ---\n${tail}` : ''}`
|
|
346
|
+
});
|
|
347
|
+
try { fsSync.closeSync(childLogFd) } catch (_) {}
|
|
348
|
+
try { fsSync.unlinkSync(notifyPath) } catch (_) {}
|
|
349
|
+
finish();
|
|
350
|
+
release();
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
const poll = setInterval(() => {
|
|
354
|
+
if (Date.now() - start > RESTART_TIMEOUT_MS) {
|
|
355
|
+
clearInterval(poll);
|
|
356
|
+
childDead = true; // 阻止 exit 监听器再发一次 error,避免和 timeout 重复
|
|
357
|
+
try { child.kill('SIGTERM') } catch (_) {}
|
|
358
|
+
// 读子进程日志尾部,帮助诊断为啥超时
|
|
359
|
+
let tail = '';
|
|
360
|
+
try { tail = fsSync.readFileSync(childLogFile, 'utf8').slice(-2000) } catch (_) {}
|
|
361
|
+
send({
|
|
362
|
+
type: 'error',
|
|
363
|
+
message: `子进程 ${RESTART_TIMEOUT_MS / 1000}s 内未就绪(未读到 ${notifyPath}),已 kill${tail ? `\n--- 子进程日志尾部 ---\n${tail}` : ''}`
|
|
364
|
+
});
|
|
365
|
+
send({ type: 'timeout' });
|
|
366
|
+
finish();
|
|
367
|
+
try { fsSync.closeSync(childLogFd) } catch (_) {}
|
|
368
|
+
try { fsSync.unlinkSync(notifyPath) } catch (_) {}
|
|
369
|
+
try { fsSync.unlinkSync(childLogFile) } catch (_) {}
|
|
370
|
+
// 关键:超时分支旧进程不退出,用户仍可继续手动刷新页面
|
|
371
|
+
release();
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// 优先判定:子进程是否写了 notify 文件(最可靠,不走 instanceRegistry)
|
|
376
|
+
let notifiedPort = null;
|
|
377
|
+
try {
|
|
378
|
+
const raw = fsSync.readFileSync(notifyPath, 'utf8').trim();
|
|
379
|
+
const p = Number(raw);
|
|
380
|
+
if (Number.isInteger(p) && p > 0 && p < 65536) notifiedPort = p;
|
|
381
|
+
} catch (_) {}
|
|
382
|
+
|
|
383
|
+
// 兜底:instanceRegistry 里出现的新 pid + port(用于没有 savePort 的环境)
|
|
384
|
+
let registryPort = null;
|
|
385
|
+
if (!notifiedPort) {
|
|
386
|
+
const after = readRegistrySync(registryPath);
|
|
387
|
+
for (const [pid, entry] of after) {
|
|
388
|
+
if (
|
|
389
|
+
!before.has(pid) &&
|
|
390
|
+
Number.isInteger(entry.port) && entry.port > 0 &&
|
|
391
|
+
(entry.lastHeartbeat ?? entry.startedAt ?? 0) >= spawnAt
|
|
392
|
+
) {
|
|
393
|
+
registryPort = entry.port;
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const newPort = notifiedPort ?? registryPort;
|
|
400
|
+
if (newPort) {
|
|
401
|
+
clearInterval(poll);
|
|
402
|
+
childDead = true; // 子进程已就绪,阻止 exit 监听器误报 error
|
|
403
|
+
// 给 EXIT_DELAY_MS 让新进程稳定,再通知前端 + 旧进程退出
|
|
404
|
+
setTimeout(() => {
|
|
405
|
+
send({ type: 'ready', port: newPort });
|
|
406
|
+
finish();
|
|
407
|
+
try { fsSync.unlinkSync(notifyPath) } catch (_) {}
|
|
408
|
+
try { fsSync.closeSync(childLogFd) } catch (_) {}
|
|
409
|
+
// 成功分支不删 childLogFile:子进程仍可能往里写新日志,便于下次诊断;交给 OS 临时文件清理
|
|
410
|
+
logger.info('[app-restart] 子进程已就绪,退出当前进程');
|
|
411
|
+
process.exit(0);
|
|
412
|
+
}, EXIT_DELAY_MS);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
}, REGISTRY_POLL_MS);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
// 读取 package.json 文件内容
|
|
420
|
+
app.post('/api/read-package-json', express.json(), async (req, res) => {
|
|
421
|
+
try {
|
|
422
|
+
const { packageJsonPath } = req.body
|
|
423
|
+
|
|
424
|
+
// 确定 package.json 的路径
|
|
425
|
+
let pkgPath
|
|
426
|
+
if (packageJsonPath && packageJsonPath.trim()) {
|
|
427
|
+
pkgPath = path.isAbsolute(packageJsonPath)
|
|
428
|
+
? packageJsonPath
|
|
429
|
+
: path.join(getCurrentProjectPath(), packageJsonPath)
|
|
430
|
+
} else {
|
|
431
|
+
pkgPath = path.join(getCurrentProjectPath(), 'package.json')
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// 检查文件是否存在
|
|
435
|
+
try {
|
|
436
|
+
const stats = await fs.stat(pkgPath)
|
|
437
|
+
if (stats.isDirectory()) {
|
|
438
|
+
pkgPath = path.join(pkgPath, 'package.json')
|
|
439
|
+
}
|
|
440
|
+
await fs.access(pkgPath)
|
|
441
|
+
} catch (err) {
|
|
442
|
+
return res.status(404).json({
|
|
443
|
+
success: false,
|
|
444
|
+
error: `未找到 package.json 文件: ${pkgPath}`
|
|
445
|
+
})
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// 读取 package.json
|
|
449
|
+
const pkgContent = await fs.readFile(pkgPath, 'utf8')
|
|
450
|
+
const pkg = JSON.parse(pkgContent)
|
|
451
|
+
|
|
452
|
+
res.json({
|
|
453
|
+
success: true,
|
|
454
|
+
dependencies: pkg.dependencies || {},
|
|
455
|
+
devDependencies: pkg.devDependencies || {},
|
|
456
|
+
peerDependencies: pkg.peerDependencies || {},
|
|
457
|
+
peerDependenciesMeta: pkg.peerDependenciesMeta || {},
|
|
458
|
+
version: pkg.version
|
|
459
|
+
})
|
|
460
|
+
} catch (error) {
|
|
461
|
+
res.status(500).json({ success: false, error: error.message })
|
|
462
|
+
}
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
// 版本号递增或依赖版本修改
|
|
466
|
+
app.post('/api/version-bump', express.json(), async (req, res) => {
|
|
467
|
+
try {
|
|
468
|
+
const { bumpType, packageJsonPath, versionTarget, dependencyName, dependencyVersion, dependencyVersionBump, dependencyType, versionValue } = req.body
|
|
469
|
+
|
|
470
|
+
// 确定 package.json 的路径
|
|
471
|
+
let pkgPath
|
|
472
|
+
if (packageJsonPath && packageJsonPath.trim()) {
|
|
473
|
+
pkgPath = path.isAbsolute(packageJsonPath)
|
|
474
|
+
? packageJsonPath
|
|
475
|
+
: path.join(getCurrentProjectPath(), packageJsonPath)
|
|
476
|
+
} else {
|
|
477
|
+
pkgPath = path.join(getCurrentProjectPath(), 'package.json')
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// 检查文件是否存在(使用 Node.js 原生方法)
|
|
481
|
+
try {
|
|
482
|
+
await fs.access(pkgPath)
|
|
483
|
+
} catch (err) {
|
|
484
|
+
return res.status(404).json({
|
|
485
|
+
success: false,
|
|
486
|
+
error: `未找到 package.json 文件: ${pkgPath}`
|
|
487
|
+
})
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// 读取 package.json
|
|
491
|
+
const pkgContent = await fs.readFile(pkgPath, 'utf8')
|
|
492
|
+
const pkg = JSON.parse(pkgContent)
|
|
493
|
+
|
|
494
|
+
// 判断是修改 version 还是 dependency
|
|
495
|
+
if (versionTarget === 'dependency') {
|
|
496
|
+
// 修改依赖版本
|
|
497
|
+
const depType = dependencyType || 'dependencies'
|
|
498
|
+
|
|
499
|
+
if (!pkg[depType]) {
|
|
500
|
+
return res.status(400).json({
|
|
501
|
+
success: false,
|
|
502
|
+
error: `package.json 中未找到 ${depType} 字段`
|
|
503
|
+
})
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (!pkg[depType][dependencyName]) {
|
|
507
|
+
return res.status(400).json({
|
|
508
|
+
success: false,
|
|
509
|
+
error: `在 ${depType} 中未找到依赖包: ${dependencyName}`
|
|
510
|
+
})
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const oldVersion = pkg[depType][dependencyName]
|
|
514
|
+
let newVersion
|
|
515
|
+
|
|
516
|
+
// 判断是自动递增还是手动输入
|
|
517
|
+
if (dependencyVersionBump) {
|
|
518
|
+
// 自动递增模式:解析当前版本号并递增
|
|
519
|
+
// 提取版本号中的数字部分(去除 ^, ~, >=, 等前缀)
|
|
520
|
+
const versionMatch = oldVersion.match(/(\^|~|>=|>|<=|<)?(\d+\.\d+\.\d+)/)
|
|
521
|
+
if (!versionMatch) {
|
|
522
|
+
return res.status(400).json({
|
|
523
|
+
success: false,
|
|
524
|
+
error: `无法解析版本号: ${oldVersion},应为 x.y.z 格式(可带 ^, ~ 等前缀)`
|
|
525
|
+
})
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const prefix = versionMatch[1] || ''
|
|
529
|
+
const versionNumber = versionMatch[2]
|
|
530
|
+
const versionParts = versionNumber.split('.').map(Number)
|
|
531
|
+
|
|
532
|
+
if (versionParts.length !== 3 || versionParts.some(isNaN)) {
|
|
533
|
+
return res.status(400).json({
|
|
534
|
+
success: false,
|
|
535
|
+
error: `无效的版本号格式: ${versionNumber}`
|
|
536
|
+
})
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// 根据类型递增版本号
|
|
540
|
+
let [major, minor, patch] = versionParts
|
|
541
|
+
if (dependencyVersionBump === 'major') {
|
|
542
|
+
major += 1
|
|
543
|
+
minor = 0
|
|
544
|
+
patch = 0
|
|
545
|
+
} else if (dependencyVersionBump === 'minor') {
|
|
546
|
+
minor += 1
|
|
547
|
+
patch = 0
|
|
548
|
+
} else { // patch
|
|
549
|
+
patch += 1
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
newVersion = `${prefix}${major}.${minor}.${patch}`
|
|
553
|
+
} else {
|
|
554
|
+
// 手动输入模式
|
|
555
|
+
newVersion = dependencyVersion
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
pkg[depType][dependencyName] = newVersion
|
|
559
|
+
|
|
560
|
+
// 写回 package.json(保持格式化)
|
|
561
|
+
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8')
|
|
562
|
+
|
|
563
|
+
res.json({
|
|
564
|
+
success: true,
|
|
565
|
+
oldVersion,
|
|
566
|
+
newVersion,
|
|
567
|
+
filePath: pkgPath,
|
|
568
|
+
dependencyName,
|
|
569
|
+
dependencyType: depType
|
|
570
|
+
})
|
|
571
|
+
} else {
|
|
572
|
+
// 修改 version 字段
|
|
573
|
+
if (!pkg.version) {
|
|
574
|
+
return res.status(400).json({
|
|
575
|
+
success: false,
|
|
576
|
+
error: 'package.json 中未找到 version 字段'
|
|
577
|
+
})
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const oldVersion = pkg.version
|
|
581
|
+
|
|
582
|
+
// 直接设置版本号(来自版本节点输入配置)
|
|
583
|
+
const direct = typeof versionValue === 'string' ? versionValue.trim() : ''
|
|
584
|
+
if (direct) {
|
|
585
|
+
const versionParts = direct.split('.').map(Number)
|
|
586
|
+
if (versionParts.length !== 3 || versionParts.some(isNaN)) {
|
|
587
|
+
return res.status(400).json({
|
|
588
|
+
success: false,
|
|
589
|
+
error: `无效的版本号格式: ${direct},应为 x.y.z 格式`
|
|
590
|
+
})
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
pkg.version = direct
|
|
594
|
+
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8')
|
|
595
|
+
|
|
596
|
+
return res.json({
|
|
597
|
+
success: true,
|
|
598
|
+
oldVersion,
|
|
599
|
+
newVersion: direct,
|
|
600
|
+
filePath: pkgPath
|
|
601
|
+
})
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// 解析版本号
|
|
605
|
+
const versionParts = oldVersion.split('.').map(Number)
|
|
606
|
+
if (versionParts.length !== 3 || versionParts.some(isNaN)) {
|
|
607
|
+
return res.status(400).json({
|
|
608
|
+
success: false,
|
|
609
|
+
error: `无效的版本号格式: ${oldVersion},应为 x.y.z 格式`
|
|
610
|
+
})
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// 根据类型递增版本号
|
|
614
|
+
let [major, minor, patch] = versionParts
|
|
615
|
+
if (bumpType === 'major') {
|
|
616
|
+
major += 1
|
|
617
|
+
minor = 0
|
|
618
|
+
patch = 0
|
|
619
|
+
} else if (bumpType === 'minor') {
|
|
620
|
+
minor += 1
|
|
621
|
+
patch = 0
|
|
622
|
+
} else { // patch
|
|
623
|
+
patch += 1
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const newVersion = `${major}.${minor}.${patch}`
|
|
627
|
+
pkg.version = newVersion
|
|
628
|
+
|
|
629
|
+
// 写回 package.json(保持格式化)
|
|
630
|
+
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8')
|
|
631
|
+
|
|
632
|
+
res.json({
|
|
633
|
+
success: true,
|
|
634
|
+
oldVersion,
|
|
635
|
+
newVersion,
|
|
636
|
+
filePath: pkgPath
|
|
637
|
+
})
|
|
638
|
+
}
|
|
639
|
+
} catch (error) {
|
|
640
|
+
res.status(500).json({ success: false, error: error.message })
|
|
641
|
+
}
|
|
642
|
+
})
|
|
643
|
+
|
|
644
|
+
// ========== NPM 脚本管理相关 API ==========
|
|
645
|
+
|
|
646
|
+
// 存储正在进行的扫描任务
|
|
647
|
+
let currentScanAbortController = null;
|
|
648
|
+
|
|
649
|
+
// 扫描项目目录及子目录下的所有package.json,并提取scripts
|
|
650
|
+
app.get('/api/scan-npm-scripts', async (req, res) => {
|
|
651
|
+
// 取消之前的扫描
|
|
652
|
+
if (currentScanAbortController) {
|
|
653
|
+
currentScanAbortController.aborted = true;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// 创建新的abort controller
|
|
657
|
+
currentScanAbortController = {
|
|
658
|
+
aborted: false,
|
|
659
|
+
abort() { this.aborted = true; }
|
|
660
|
+
};
|
|
661
|
+
const scanController = currentScanAbortController;
|
|
662
|
+
try {
|
|
663
|
+
const projectRoot = process.cwd();
|
|
664
|
+
const packageJsons = [];
|
|
665
|
+
const startTime = Date.now();
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
// 需要忽略的目录列表(更全面)
|
|
669
|
+
const IGNORED_DIRS = new Set([
|
|
670
|
+
'node_modules',
|
|
671
|
+
'.git',
|
|
672
|
+
'.svn',
|
|
673
|
+
'.hg',
|
|
674
|
+
'dist',
|
|
675
|
+
'build',
|
|
676
|
+
'coverage',
|
|
677
|
+
'out',
|
|
678
|
+
'target',
|
|
679
|
+
'vendor',
|
|
680
|
+
'__pycache__',
|
|
681
|
+
'.next',
|
|
682
|
+
'.nuxt',
|
|
683
|
+
'.vscode',
|
|
684
|
+
'.idea',
|
|
685
|
+
'tmp',
|
|
686
|
+
'temp',
|
|
687
|
+
'cache',
|
|
688
|
+
'.cache'
|
|
689
|
+
]);
|
|
690
|
+
|
|
691
|
+
// 优先扫描的子目录(monorepo常见结构)
|
|
692
|
+
const PRIORITY_DIRS = ['packages', 'apps', 'libs', 'services', 'modules'];
|
|
693
|
+
|
|
694
|
+
let scannedCount = 0;
|
|
695
|
+
let skippedCount = 0;
|
|
696
|
+
let fileReadCount = 0; // 统计实际读取的文件数量
|
|
697
|
+
|
|
698
|
+
// 检查指定目录下是否有package.json
|
|
699
|
+
async function checkPackageJson(dir) {
|
|
700
|
+
if (scanController.aborted) return false;
|
|
701
|
+
|
|
702
|
+
try {
|
|
703
|
+
const packagePath = path.join(dir, 'package.json');
|
|
704
|
+
|
|
705
|
+
// 先检查文件是否存在,避免不必要的读取
|
|
706
|
+
try {
|
|
707
|
+
await fs.access(packagePath);
|
|
708
|
+
} catch {
|
|
709
|
+
// 文件不存在,直接返回
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// 检查文件大小,避免读取异常大的文件
|
|
714
|
+
const stats = await fs.stat(packagePath);
|
|
715
|
+
const fileSizeMB = stats.size / (1024 * 1024);
|
|
716
|
+
if (fileSizeMB > 1) {
|
|
717
|
+
// package.json超过1MB是异常情况,跳过
|
|
718
|
+
logger.info(`[NPM扫描] 跳过超大文件 (${fileSizeMB.toFixed(2)}MB): ${packagePath}`);
|
|
719
|
+
return false;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
fileReadCount++; // 只有文件存在且大小合理时才计数
|
|
723
|
+
const content = await fs.readFile(packagePath, 'utf8');
|
|
724
|
+
const packageData = JSON.parse(content);
|
|
725
|
+
|
|
726
|
+
// 只有当scripts存在且至少有一个脚本时才添加
|
|
727
|
+
if (packageData.scripts && Object.keys(packageData.scripts).length > 0) {
|
|
728
|
+
const relativePath = path.relative(projectRoot, dir);
|
|
729
|
+
packageJsons.push({
|
|
730
|
+
path: dir,
|
|
731
|
+
relativePath: relativePath || '.',
|
|
732
|
+
name: packageData.name || path.basename(dir),
|
|
733
|
+
scripts: packageData.scripts,
|
|
734
|
+
version: packageData.version || '0.0.0',
|
|
735
|
+
repository: packageData.repository
|
|
736
|
+
});
|
|
737
|
+
return true;
|
|
738
|
+
}
|
|
739
|
+
} catch (error) {
|
|
740
|
+
// 文件不存在或解析失败,忽略
|
|
741
|
+
}
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// 递归扫描目录,最大深度4层
|
|
746
|
+
const MAX_DEPTH = 4;
|
|
747
|
+
const MAX_DIRS_PER_LEVEL = 50; // 每层最多扫描50个子目录
|
|
748
|
+
|
|
749
|
+
// 统计每层深度的扫描数量
|
|
750
|
+
const depthStats = Array(MAX_DEPTH + 1).fill(0).map(() => ({ count: 0, time: 0 }));
|
|
751
|
+
|
|
752
|
+
async function scanDirectory(dir, depth = 0) {
|
|
753
|
+
if (scanController.aborted) return;
|
|
754
|
+
if (depth > MAX_DEPTH) return;
|
|
755
|
+
|
|
756
|
+
const depthStart = Date.now();
|
|
757
|
+
scannedCount++;
|
|
758
|
+
depthStats[depth].count++;
|
|
759
|
+
|
|
760
|
+
// 检查当前目录的package.json
|
|
761
|
+
await checkPackageJson(dir);
|
|
762
|
+
|
|
763
|
+
// 如果已经达到最大深度,不再继续
|
|
764
|
+
if (depth >= MAX_DEPTH) return;
|
|
765
|
+
|
|
766
|
+
// 读取子目录
|
|
767
|
+
try {
|
|
768
|
+
if (scanController.aborted) return;
|
|
769
|
+
|
|
770
|
+
const items = await fs.readdir(dir, { withFileTypes: true });
|
|
771
|
+
const subDirs = [];
|
|
772
|
+
|
|
773
|
+
// 收集所有子目录
|
|
774
|
+
for (const item of items) {
|
|
775
|
+
if (scanController.aborted) return;
|
|
776
|
+
if (!item.isDirectory()) continue;
|
|
777
|
+
|
|
778
|
+
const dirName = item.name;
|
|
779
|
+
|
|
780
|
+
// 跳过忽略的目录
|
|
781
|
+
if (IGNORED_DIRS.has(dirName) || dirName.startsWith('.')) {
|
|
782
|
+
skippedCount++;
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
subDirs.push(item);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// 限制每层扫描的子目录数量
|
|
790
|
+
const dirsToScan = subDirs.slice(0, MAX_DIRS_PER_LEVEL);
|
|
791
|
+
if (subDirs.length > MAX_DIRS_PER_LEVEL) {
|
|
792
|
+
skippedCount += subDirs.length - MAX_DIRS_PER_LEVEL;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// 优先处理优先目录
|
|
796
|
+
const priorityDirs = dirsToScan.filter(item => PRIORITY_DIRS.includes(item.name));
|
|
797
|
+
const normalDirs = dirsToScan.filter(item => !PRIORITY_DIRS.includes(item.name));
|
|
798
|
+
|
|
799
|
+
// 先扫描优先目录
|
|
800
|
+
for (const item of priorityDirs) {
|
|
801
|
+
if (scanController.aborted) return;
|
|
802
|
+
const subDirPath = path.join(dir, item.name);
|
|
803
|
+
await scanDirectory(subDirPath, depth + 1);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// 再扫描普通目录
|
|
807
|
+
for (const item of normalDirs) {
|
|
808
|
+
if (scanController.aborted) return;
|
|
809
|
+
const subDirPath = path.join(dir, item.name);
|
|
810
|
+
await scanDirectory(subDirPath, depth + 1);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
} catch (error) {
|
|
814
|
+
// 忽略无法访问的目录
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// 记录该深度的耗时
|
|
818
|
+
depthStats[depth].time += Date.now() - depthStart;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// 执行递归扫描
|
|
822
|
+
const scanStart = Date.now();
|
|
823
|
+
await scanDirectory(projectRoot, 0);
|
|
824
|
+
|
|
825
|
+
// 扫描完成,清除abort controller
|
|
826
|
+
if (currentScanAbortController === scanController) {
|
|
827
|
+
currentScanAbortController = null;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const scanTime = Date.now() - startTime;
|
|
831
|
+
|
|
832
|
+
if (scanController.aborted) {
|
|
833
|
+
logger.info(`npm脚本扫描被取消,耗时${scanTime}ms`);
|
|
834
|
+
return res.json({
|
|
835
|
+
success: true,
|
|
836
|
+
packages: [],
|
|
837
|
+
totalScripts: 0,
|
|
838
|
+
cancelled: true
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// 输出每层深度的统计
|
|
843
|
+
const depthInfo = depthStats
|
|
844
|
+
.map((stat, depth) => stat.count > 0 ? `深度${depth}:${stat.count}个(${stat.time}ms)` : null)
|
|
845
|
+
.filter(Boolean)
|
|
846
|
+
.join(', ');
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
res.json({
|
|
850
|
+
success: true,
|
|
851
|
+
packages: packageJsons,
|
|
852
|
+
totalScripts: packageJsons.reduce((sum, pkg) => sum + Object.keys(pkg.scripts).length, 0)
|
|
853
|
+
});
|
|
854
|
+
} catch (error) {
|
|
855
|
+
logger.error('扫描npm脚本失败:', error);
|
|
856
|
+
res.status(500).json({
|
|
857
|
+
success: false,
|
|
858
|
+
error: `扫描npm脚本失败: ${error.message}`
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
// 扫描项目目录下的所有package.json文件(用于版本管理)
|
|
864
|
+
app.get('/api/scan-package-files', async (req, res) => {
|
|
865
|
+
try {
|
|
866
|
+
// 支持通过查询参数指定扫描目录,如果没有指定则使用当前工作目录
|
|
867
|
+
const customDirectory = req.query.directory;
|
|
868
|
+
const projectRoot = customDirectory || process.cwd();
|
|
869
|
+
|
|
870
|
+
// 验证目录是否存在且可访问
|
|
871
|
+
try {
|
|
872
|
+
const stats = await fs.stat(projectRoot);
|
|
873
|
+
if (!stats.isDirectory()) {
|
|
874
|
+
return res.status(400).json({
|
|
875
|
+
success: false,
|
|
876
|
+
error: '指定的路径不是一个有效的目录'
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
} catch (error) {
|
|
880
|
+
return res.status(400).json({
|
|
881
|
+
success: false,
|
|
882
|
+
error: `无法访问指定的目录: ${error.message}`
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const packageFiles = [];
|
|
887
|
+
const startTime = Date.now();
|
|
888
|
+
|
|
889
|
+
// 需要忽略的目录列表
|
|
890
|
+
const IGNORED_DIRS = new Set([
|
|
891
|
+
'node_modules',
|
|
892
|
+
'.git',
|
|
893
|
+
'.svn',
|
|
894
|
+
'.hg',
|
|
895
|
+
'dist',
|
|
896
|
+
'build',
|
|
897
|
+
'coverage',
|
|
898
|
+
'out',
|
|
899
|
+
'target',
|
|
900
|
+
'vendor',
|
|
901
|
+
'__pycache__',
|
|
902
|
+
'.next',
|
|
903
|
+
'.nuxt',
|
|
904
|
+
'.vscode',
|
|
905
|
+
'.idea',
|
|
906
|
+
'tmp',
|
|
907
|
+
'temp',
|
|
908
|
+
'cache',
|
|
909
|
+
'.cache'
|
|
910
|
+
]);
|
|
911
|
+
|
|
912
|
+
let scannedCount = 0;
|
|
913
|
+
let fileReadCount = 0;
|
|
914
|
+
|
|
915
|
+
// 检查指定目录下是否有package.json
|
|
916
|
+
async function checkPackageJson(dir) {
|
|
917
|
+
try {
|
|
918
|
+
const packagePath = path.join(dir, 'package.json');
|
|
919
|
+
|
|
920
|
+
// 先检查文件是否存在
|
|
921
|
+
try {
|
|
922
|
+
await fs.access(packagePath);
|
|
923
|
+
} catch {
|
|
924
|
+
return false;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// 检查文件大小
|
|
928
|
+
const stats = await fs.stat(packagePath);
|
|
929
|
+
const fileSizeMB = stats.size / (1024 * 1024);
|
|
930
|
+
if (fileSizeMB > 1) {
|
|
931
|
+
return false;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
fileReadCount++;
|
|
935
|
+
const content = await fs.readFile(packagePath, 'utf8');
|
|
936
|
+
const packageData = JSON.parse(content);
|
|
937
|
+
|
|
938
|
+
// 添加所有有效的package.json文件(不仅仅是有scripts的)
|
|
939
|
+
if (packageData.name || packageData.version) {
|
|
940
|
+
const relativePath = path.relative(projectRoot, dir);
|
|
941
|
+
packageFiles.push({
|
|
942
|
+
path: dir,
|
|
943
|
+
relativePath: relativePath || '.',
|
|
944
|
+
name: packageData.name || path.basename(dir),
|
|
945
|
+
version: packageData.version || '0.0.0',
|
|
946
|
+
displayName: packageData.name ? `${packageData.name} (${packageData.version || '0.0.0'})` : `${path.basename(dir)} (${packageData.version || '0.0.0'})`,
|
|
947
|
+
fullPath: packagePath
|
|
948
|
+
});
|
|
949
|
+
return true;
|
|
950
|
+
}
|
|
951
|
+
} catch (error) {
|
|
952
|
+
// 文件不存在或解析失败,忽略
|
|
953
|
+
}
|
|
954
|
+
return false;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// 递归扫描目录,最大深度4层
|
|
958
|
+
const MAX_DEPTH = 4;
|
|
959
|
+
const MAX_DIRS_PER_LEVEL = 50;
|
|
960
|
+
|
|
961
|
+
async function scanDirectory(dir, depth = 0) {
|
|
962
|
+
if (depth > MAX_DEPTH) return;
|
|
963
|
+
|
|
964
|
+
scannedCount++;
|
|
965
|
+
|
|
966
|
+
// 检查当前目录的package.json
|
|
967
|
+
await checkPackageJson(dir);
|
|
968
|
+
|
|
969
|
+
// 如果已经达到最大深度,不再继续
|
|
970
|
+
if (depth >= MAX_DEPTH) return;
|
|
971
|
+
|
|
972
|
+
// 读取子目录
|
|
973
|
+
try {
|
|
974
|
+
const items = await fs.readdir(dir, { withFileTypes: true });
|
|
975
|
+
const subDirs = [];
|
|
976
|
+
|
|
977
|
+
// 收集所有子目录
|
|
978
|
+
for (const item of items) {
|
|
979
|
+
if (!item.isDirectory()) continue;
|
|
980
|
+
|
|
981
|
+
const dirName = item.name;
|
|
982
|
+
|
|
983
|
+
// 跳过忽略的目录
|
|
984
|
+
if (IGNORED_DIRS.has(dirName) || dirName.startsWith('.')) {
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
subDirs.push(item);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// 限制每层扫描的子目录数量
|
|
992
|
+
const dirsToScan = subDirs.slice(0, MAX_DIRS_PER_LEVEL);
|
|
993
|
+
|
|
994
|
+
// 递归扫描子目录
|
|
995
|
+
for (const item of dirsToScan) {
|
|
996
|
+
const subDirPath = path.join(dir, item.name);
|
|
997
|
+
await scanDirectory(subDirPath, depth + 1);
|
|
998
|
+
}
|
|
999
|
+
} catch (error) {
|
|
1000
|
+
// 忽略无法读取的目录
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// 开始扫描
|
|
1005
|
+
await scanDirectory(projectRoot);
|
|
1006
|
+
|
|
1007
|
+
const scanTime = Date.now() - startTime;
|
|
1008
|
+
|
|
1009
|
+
res.json({
|
|
1010
|
+
success: true,
|
|
1011
|
+
packages: packageFiles,
|
|
1012
|
+
scanTime,
|
|
1013
|
+
scannedCount,
|
|
1014
|
+
fileReadCount
|
|
1015
|
+
});
|
|
1016
|
+
} catch (error) {
|
|
1017
|
+
logger.error('扫描package.json文件失败:', error);
|
|
1018
|
+
res.status(500).json({
|
|
1019
|
+
success: false,
|
|
1020
|
+
error: `扫描package.json文件失败: ${error.message}`
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
// 在新终端中执行npm脚本
|
|
1026
|
+
app.post('/api/run-npm-script', async (req, res) => {
|
|
1027
|
+
try {
|
|
1028
|
+
const { packagePath, scriptName } = req.body;
|
|
1029
|
+
|
|
1030
|
+
if (!packagePath || !scriptName) {
|
|
1031
|
+
return res.status(400).json({
|
|
1032
|
+
success: false,
|
|
1033
|
+
error: '缺少必要参数:packagePath 和 scriptName'
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
|
|
977
1037
|
logger.info(`执行npm脚本: ${scriptName} in ${packagePath}`);
|
|
978
1038
|
|
|
979
1039
|
// SEC-INJ-5 防御:scriptName 严格白名单(只允许 package.json scripts key),
|
|
@@ -1037,354 +1097,354 @@ export function registerNpmRoutes({
|
|
|
1037
1097
|
message: `已在新终端中执行: ${scriptName}`,
|
|
1038
1098
|
command: npmCommand,
|
|
1039
1099
|
path: resolvedPkgPath
|
|
1040
|
-
});
|
|
1041
|
-
|
|
1042
|
-
res.json({
|
|
1043
|
-
success: true,
|
|
1044
|
-
message: `已在新终端中执行: ${scriptName}`,
|
|
1045
|
-
command: npmCommand,
|
|
1046
|
-
path: packagePath
|
|
1047
|
-
});
|
|
1048
|
-
} catch (error) {
|
|
1049
|
-
logger.error('执行npm脚本失败:', error);
|
|
1050
|
-
res.status(500).json({
|
|
1051
|
-
success: false,
|
|
1052
|
-
error: `执行npm脚本失败: ${error.message}`
|
|
1053
|
-
});
|
|
1054
|
-
}
|
|
1055
|
-
});
|
|
1056
|
-
|
|
1057
|
-
// API: 更新npm版本号
|
|
1058
|
-
app.post('/api/update-npm-version', async (req, res) => {
|
|
1059
|
-
try {
|
|
1060
|
-
const { packagePath, versionType } = req.body;
|
|
1061
|
-
|
|
1062
|
-
if (!packagePath || !versionType) {
|
|
1063
|
-
return res.status(400).json({
|
|
1064
|
-
success: false,
|
|
1065
|
-
error: '缺少必要参数: packagePath, versionType'
|
|
1066
|
-
});
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
// 确保路径指向package.json文件
|
|
1070
|
-
let packageJsonPath = path.resolve(packagePath);
|
|
1071
|
-
if (fsSync.existsSync(packageJsonPath) && fsSync.statSync(packageJsonPath).isDirectory()) {
|
|
1072
|
-
packageJsonPath = path.join(packageJsonPath, 'package.json');
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
// 检查文件是否存在
|
|
1076
|
-
if (!fsSync.existsSync(packageJsonPath)) {
|
|
1077
|
-
return res.status(404).json({
|
|
1078
|
-
success: false,
|
|
1079
|
-
error: '找不到package.json文件'
|
|
1080
|
-
});
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
// 读取package.json
|
|
1084
|
-
const packageJson = JSON.parse(fsSync.readFileSync(packageJsonPath, 'utf8'));
|
|
1085
|
-
|
|
1086
|
-
if (!packageJson.version) {
|
|
1087
|
-
return res.status(400).json({
|
|
1088
|
-
success: false,
|
|
1089
|
-
error: 'package.json中没有version字段'
|
|
1090
|
-
});
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
const oldVersion = packageJson.version;
|
|
1094
|
-
const versionParts = oldVersion.split('.').map(Number);
|
|
1095
|
-
|
|
1096
|
-
// 根据类型增加版本号
|
|
1097
|
-
switch (versionType) {
|
|
1098
|
-
case 'major':
|
|
1099
|
-
versionParts[0]++;
|
|
1100
|
-
versionParts[1] = 0;
|
|
1101
|
-
versionParts[2] = 0;
|
|
1102
|
-
break;
|
|
1103
|
-
case 'minor':
|
|
1104
|
-
versionParts[1]++;
|
|
1105
|
-
versionParts[2] = 0;
|
|
1106
|
-
break;
|
|
1107
|
-
case 'patch':
|
|
1108
|
-
versionParts[2]++;
|
|
1109
|
-
break;
|
|
1110
|
-
default:
|
|
1111
|
-
return res.status(400).json({
|
|
1112
|
-
success: false,
|
|
1113
|
-
error: '无效的版本类型,必须是 major, minor 或 patch'
|
|
1114
|
-
});
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
const newVersion = versionParts.join('.');
|
|
1118
|
-
packageJson.version = newVersion;
|
|
1119
|
-
|
|
1120
|
-
// 写回文件
|
|
1121
|
-
fsSync.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf8');
|
|
1122
|
-
|
|
1123
|
-
logger.info(`已更新npm版本号: ${oldVersion} → ${newVersion} (${packagePath})`);
|
|
1124
|
-
|
|
1125
|
-
res.json({
|
|
1126
|
-
success: true,
|
|
1127
|
-
oldVersion,
|
|
1128
|
-
newVersion
|
|
1129
|
-
});
|
|
1130
|
-
} catch (error) {
|
|
1131
|
-
logger.error('更新版本号失败:', error);
|
|
1132
|
-
res.status(500).json({
|
|
1133
|
-
success: false,
|
|
1134
|
-
error: `更新版本号失败: ${error.message}`
|
|
1135
|
-
});
|
|
1136
|
-
}
|
|
1137
|
-
});
|
|
1138
|
-
|
|
1139
|
-
// API: 添加npm脚本
|
|
1140
|
-
app.post('/api/add-npm-script', async (req, res) => {
|
|
1141
|
-
try {
|
|
1142
|
-
const { packagePath, scriptName, scriptCommand } = req.body;
|
|
1143
|
-
|
|
1144
|
-
if (!packagePath || !scriptName || !scriptCommand) {
|
|
1145
|
-
return res.status(400).json({
|
|
1146
|
-
success: false,
|
|
1147
|
-
error: '缺少必要参数: packagePath, scriptName, scriptCommand'
|
|
1148
|
-
});
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
// 确保路径指向package.json文件
|
|
1152
|
-
let packageJsonPath = path.resolve(packagePath);
|
|
1153
|
-
if (fsSync.existsSync(packageJsonPath) && fsSync.statSync(packageJsonPath).isDirectory()) {
|
|
1154
|
-
packageJsonPath = path.join(packageJsonPath, 'package.json');
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
// 检查文件是否存在
|
|
1158
|
-
if (!fsSync.existsSync(packageJsonPath)) {
|
|
1159
|
-
return res.status(404).json({
|
|
1160
|
-
success: false,
|
|
1161
|
-
error: '找不到package.json文件'
|
|
1162
|
-
});
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
// 读取package.json
|
|
1166
|
-
const packageJson = JSON.parse(fsSync.readFileSync(packageJsonPath, 'utf8'));
|
|
1167
|
-
|
|
1168
|
-
// 确保scripts对象存在
|
|
1169
|
-
if (!packageJson.scripts) {
|
|
1170
|
-
packageJson.scripts = {};
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
// 检查脚本是否已存在
|
|
1174
|
-
if (packageJson.scripts[scriptName]) {
|
|
1175
|
-
return res.status(400).json({
|
|
1176
|
-
success: false,
|
|
1177
|
-
error: `脚本 "${scriptName}" 已存在`
|
|
1178
|
-
});
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
// 添加脚本
|
|
1182
|
-
packageJson.scripts[scriptName] = scriptCommand;
|
|
1183
|
-
|
|
1184
|
-
// 写回文件(保持格式化)
|
|
1185
|
-
fsSync.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf8');
|
|
1186
|
-
|
|
1187
|
-
logger.info(`已添加npm脚本: ${scriptName} = ${scriptCommand} (${packagePath})`);
|
|
1188
|
-
|
|
1189
|
-
res.json({
|
|
1190
|
-
success: true,
|
|
1191
|
-
scriptName,
|
|
1192
|
-
scriptCommand
|
|
1193
|
-
});
|
|
1194
|
-
} catch (error) {
|
|
1195
|
-
logger.error('添加npm脚本失败:', error);
|
|
1196
|
-
res.status(500).json({
|
|
1197
|
-
success: false,
|
|
1198
|
-
error: `添加npm脚本失败: ${error.message}`
|
|
1199
|
-
});
|
|
1200
|
-
}
|
|
1201
|
-
});
|
|
1202
|
-
|
|
1203
|
-
// API: 更新npm脚本
|
|
1204
|
-
app.post('/api/update-npm-script', async (req, res) => {
|
|
1205
|
-
try {
|
|
1206
|
-
const { packagePath, scriptName, scriptCommand, oldScriptName } = req.body;
|
|
1207
|
-
|
|
1208
|
-
if (!packagePath || !scriptName || !scriptCommand) {
|
|
1209
|
-
return res.status(400).json({
|
|
1210
|
-
success: false,
|
|
1211
|
-
error: '缺少必要参数: packagePath, scriptName, scriptCommand'
|
|
1212
|
-
});
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
// 确保路径指向package.json文件
|
|
1216
|
-
let packageJsonPath = path.resolve(packagePath);
|
|
1217
|
-
if (fsSync.existsSync(packageJsonPath) && fsSync.statSync(packageJsonPath).isDirectory()) {
|
|
1218
|
-
packageJsonPath = path.join(packageJsonPath, 'package.json');
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
// 检查文件是否存在
|
|
1222
|
-
if (!fsSync.existsSync(packageJsonPath)) {
|
|
1223
|
-
return res.status(404).json({
|
|
1224
|
-
success: false,
|
|
1225
|
-
error: '找不到package.json文件'
|
|
1226
|
-
});
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
// 读取package.json
|
|
1230
|
-
const packageJson = JSON.parse(fsSync.readFileSync(packageJsonPath, 'utf8'));
|
|
1231
|
-
|
|
1232
|
-
// 确保scripts对象存在
|
|
1233
|
-
if (!packageJson.scripts) {
|
|
1234
|
-
packageJson.scripts = {};
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
// 如果改了脚本名称,删除旧的
|
|
1238
|
-
if (oldScriptName && oldScriptName !== scriptName) {
|
|
1239
|
-
delete packageJson.scripts[oldScriptName];
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
// 更新脚本
|
|
1243
|
-
packageJson.scripts[scriptName] = scriptCommand;
|
|
1244
|
-
|
|
1245
|
-
// 写回文件
|
|
1246
|
-
fsSync.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf8');
|
|
1247
|
-
|
|
1248
|
-
logger.info(`已更新npm脚本: ${scriptName} = ${scriptCommand} (${packagePath})`);
|
|
1249
|
-
|
|
1250
|
-
res.json({
|
|
1251
|
-
success: true,
|
|
1252
|
-
scriptName,
|
|
1253
|
-
scriptCommand
|
|
1254
|
-
});
|
|
1255
|
-
} catch (error) {
|
|
1256
|
-
logger.error('更新npm脚本失败:', error);
|
|
1257
|
-
res.status(500).json({
|
|
1258
|
-
success: false,
|
|
1259
|
-
error: `更新npm脚本失败: ${error.message}`
|
|
1260
|
-
});
|
|
1261
|
-
}
|
|
1262
|
-
});
|
|
1263
|
-
|
|
1264
|
-
// API: 删除npm脚本
|
|
1265
|
-
app.post('/api/delete-npm-script', async (req, res) => {
|
|
1266
|
-
try {
|
|
1267
|
-
const { packagePath, scriptName } = req.body;
|
|
1268
|
-
|
|
1269
|
-
if (!packagePath || !scriptName) {
|
|
1270
|
-
return res.status(400).json({
|
|
1271
|
-
success: false,
|
|
1272
|
-
error: '缺少必要参数: packagePath, scriptName'
|
|
1273
|
-
});
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
// 确保路径指向package.json文件
|
|
1277
|
-
let packageJsonPath = path.resolve(packagePath);
|
|
1278
|
-
if (fsSync.existsSync(packageJsonPath) && fsSync.statSync(packageJsonPath).isDirectory()) {
|
|
1279
|
-
packageJsonPath = path.join(packageJsonPath, 'package.json');
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
// 检查文件是否存在
|
|
1283
|
-
if (!fsSync.existsSync(packageJsonPath)) {
|
|
1284
|
-
return res.status(404).json({
|
|
1285
|
-
success: false,
|
|
1286
|
-
error: '找不到package.json文件'
|
|
1287
|
-
});
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
// 读取package.json
|
|
1291
|
-
const packageJson = JSON.parse(fsSync.readFileSync(packageJsonPath, 'utf8'));
|
|
1292
|
-
|
|
1293
|
-
// 检查scripts对象和脚本是否存在
|
|
1294
|
-
if (!packageJson.scripts || !packageJson.scripts[scriptName]) {
|
|
1295
|
-
return res.status(404).json({
|
|
1296
|
-
success: false,
|
|
1297
|
-
error: `脚本 "${scriptName}" 不存在`
|
|
1298
|
-
});
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
// 删除脚本
|
|
1302
|
-
delete packageJson.scripts[scriptName];
|
|
1303
|
-
|
|
1304
|
-
// 写回文件
|
|
1305
|
-
fsSync.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf8');
|
|
1306
|
-
|
|
1307
|
-
logger.info(`已删除npm脚本: ${scriptName} (${packagePath})`);
|
|
1308
|
-
|
|
1309
|
-
res.json({
|
|
1310
|
-
success: true,
|
|
1311
|
-
scriptName
|
|
1312
|
-
});
|
|
1313
|
-
} catch (error) {
|
|
1314
|
-
logger.error('删除npm脚本失败:', error);
|
|
1315
|
-
res.status(500).json({
|
|
1316
|
-
success: false,
|
|
1317
|
-
error: `删除npm脚本失败: ${error.message}`
|
|
1318
|
-
});
|
|
1319
|
-
}
|
|
1320
|
-
});
|
|
1321
|
-
|
|
1322
|
-
// API: 置顶npm脚本
|
|
1323
|
-
app.post('/api/pin-npm-script', async (req, res) => {
|
|
1324
|
-
try {
|
|
1325
|
-
const { packagePath, scriptName } = req.body;
|
|
1326
|
-
|
|
1327
|
-
if (!packagePath || !scriptName) {
|
|
1328
|
-
return res.status(400).json({
|
|
1329
|
-
success: false,
|
|
1330
|
-
error: '缺少必要参数: packagePath, scriptName'
|
|
1331
|
-
});
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1334
|
-
// 确保路径指向package.json文件
|
|
1335
|
-
let packageJsonPath = path.resolve(packagePath);
|
|
1336
|
-
if (fsSync.existsSync(packageJsonPath) && fsSync.statSync(packageJsonPath).isDirectory()) {
|
|
1337
|
-
packageJsonPath = path.join(packageJsonPath, 'package.json');
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
// 检查文件是否存在
|
|
1341
|
-
if (!fsSync.existsSync(packageJsonPath)) {
|
|
1342
|
-
return res.status(404).json({
|
|
1343
|
-
success: false,
|
|
1344
|
-
error: '找不到package.json文件'
|
|
1345
|
-
});
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
// 读取package.json
|
|
1349
|
-
const packageJson = JSON.parse(fsSync.readFileSync(packageJsonPath, 'utf8'));
|
|
1350
|
-
|
|
1351
|
-
// 检查scripts对象和脚本是否存在
|
|
1352
|
-
if (!packageJson.scripts || !packageJson.scripts[scriptName]) {
|
|
1353
|
-
return res.status(404).json({
|
|
1354
|
-
success: false,
|
|
1355
|
-
error: `脚本 "${scriptName}" 不存在`
|
|
1356
|
-
});
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
// 保存要置顶的脚本内容
|
|
1360
|
-
const scriptCommand = packageJson.scripts[scriptName];
|
|
1361
|
-
|
|
1362
|
-
// 删除该脚本
|
|
1363
|
-
delete packageJson.scripts[scriptName];
|
|
1364
|
-
|
|
1365
|
-
// 创建新的scripts对象,将置顶脚本放在最前面
|
|
1366
|
-
const newScripts = {
|
|
1367
|
-
[scriptName]: scriptCommand,
|
|
1368
|
-
...packageJson.scripts
|
|
1369
|
-
};
|
|
1370
|
-
|
|
1371
|
-
packageJson.scripts = newScripts;
|
|
1372
|
-
|
|
1373
|
-
// 写回文件
|
|
1374
|
-
fsSync.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf8');
|
|
1375
|
-
|
|
1376
|
-
logger.info(`已置顶npm脚本: ${scriptName} (${packagePath})`);
|
|
1377
|
-
|
|
1378
|
-
res.json({
|
|
1379
|
-
success: true,
|
|
1380
|
-
scriptName
|
|
1381
|
-
});
|
|
1382
|
-
} catch (error) {
|
|
1383
|
-
logger.error('置顶npm脚本失败:', error);
|
|
1384
|
-
res.status(500).json({
|
|
1385
|
-
success: false,
|
|
1386
|
-
error: `置顶npm脚本失败: ${error.message}`
|
|
1387
|
-
});
|
|
1388
|
-
}
|
|
1389
|
-
});
|
|
1390
|
-
}
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
res.json({
|
|
1103
|
+
success: true,
|
|
1104
|
+
message: `已在新终端中执行: ${scriptName}`,
|
|
1105
|
+
command: npmCommand,
|
|
1106
|
+
path: packagePath
|
|
1107
|
+
});
|
|
1108
|
+
} catch (error) {
|
|
1109
|
+
logger.error('执行npm脚本失败:', error);
|
|
1110
|
+
res.status(500).json({
|
|
1111
|
+
success: false,
|
|
1112
|
+
error: `执行npm脚本失败: ${error.message}`
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
// API: 更新npm版本号
|
|
1118
|
+
app.post('/api/update-npm-version', async (req, res) => {
|
|
1119
|
+
try {
|
|
1120
|
+
const { packagePath, versionType } = req.body;
|
|
1121
|
+
|
|
1122
|
+
if (!packagePath || !versionType) {
|
|
1123
|
+
return res.status(400).json({
|
|
1124
|
+
success: false,
|
|
1125
|
+
error: '缺少必要参数: packagePath, versionType'
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// 确保路径指向package.json文件
|
|
1130
|
+
let packageJsonPath = path.resolve(packagePath);
|
|
1131
|
+
if (fsSync.existsSync(packageJsonPath) && fsSync.statSync(packageJsonPath).isDirectory()) {
|
|
1132
|
+
packageJsonPath = path.join(packageJsonPath, 'package.json');
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// 检查文件是否存在
|
|
1136
|
+
if (!fsSync.existsSync(packageJsonPath)) {
|
|
1137
|
+
return res.status(404).json({
|
|
1138
|
+
success: false,
|
|
1139
|
+
error: '找不到package.json文件'
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// 读取package.json
|
|
1144
|
+
const packageJson = JSON.parse(fsSync.readFileSync(packageJsonPath, 'utf8'));
|
|
1145
|
+
|
|
1146
|
+
if (!packageJson.version) {
|
|
1147
|
+
return res.status(400).json({
|
|
1148
|
+
success: false,
|
|
1149
|
+
error: 'package.json中没有version字段'
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
const oldVersion = packageJson.version;
|
|
1154
|
+
const versionParts = oldVersion.split('.').map(Number);
|
|
1155
|
+
|
|
1156
|
+
// 根据类型增加版本号
|
|
1157
|
+
switch (versionType) {
|
|
1158
|
+
case 'major':
|
|
1159
|
+
versionParts[0]++;
|
|
1160
|
+
versionParts[1] = 0;
|
|
1161
|
+
versionParts[2] = 0;
|
|
1162
|
+
break;
|
|
1163
|
+
case 'minor':
|
|
1164
|
+
versionParts[1]++;
|
|
1165
|
+
versionParts[2] = 0;
|
|
1166
|
+
break;
|
|
1167
|
+
case 'patch':
|
|
1168
|
+
versionParts[2]++;
|
|
1169
|
+
break;
|
|
1170
|
+
default:
|
|
1171
|
+
return res.status(400).json({
|
|
1172
|
+
success: false,
|
|
1173
|
+
error: '无效的版本类型,必须是 major, minor 或 patch'
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
const newVersion = versionParts.join('.');
|
|
1178
|
+
packageJson.version = newVersion;
|
|
1179
|
+
|
|
1180
|
+
// 写回文件
|
|
1181
|
+
fsSync.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf8');
|
|
1182
|
+
|
|
1183
|
+
logger.info(`已更新npm版本号: ${oldVersion} → ${newVersion} (${packagePath})`);
|
|
1184
|
+
|
|
1185
|
+
res.json({
|
|
1186
|
+
success: true,
|
|
1187
|
+
oldVersion,
|
|
1188
|
+
newVersion
|
|
1189
|
+
});
|
|
1190
|
+
} catch (error) {
|
|
1191
|
+
logger.error('更新版本号失败:', error);
|
|
1192
|
+
res.status(500).json({
|
|
1193
|
+
success: false,
|
|
1194
|
+
error: `更新版本号失败: ${error.message}`
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
// API: 添加npm脚本
|
|
1200
|
+
app.post('/api/add-npm-script', async (req, res) => {
|
|
1201
|
+
try {
|
|
1202
|
+
const { packagePath, scriptName, scriptCommand } = req.body;
|
|
1203
|
+
|
|
1204
|
+
if (!packagePath || !scriptName || !scriptCommand) {
|
|
1205
|
+
return res.status(400).json({
|
|
1206
|
+
success: false,
|
|
1207
|
+
error: '缺少必要参数: packagePath, scriptName, scriptCommand'
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// 确保路径指向package.json文件
|
|
1212
|
+
let packageJsonPath = path.resolve(packagePath);
|
|
1213
|
+
if (fsSync.existsSync(packageJsonPath) && fsSync.statSync(packageJsonPath).isDirectory()) {
|
|
1214
|
+
packageJsonPath = path.join(packageJsonPath, 'package.json');
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// 检查文件是否存在
|
|
1218
|
+
if (!fsSync.existsSync(packageJsonPath)) {
|
|
1219
|
+
return res.status(404).json({
|
|
1220
|
+
success: false,
|
|
1221
|
+
error: '找不到package.json文件'
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// 读取package.json
|
|
1226
|
+
const packageJson = JSON.parse(fsSync.readFileSync(packageJsonPath, 'utf8'));
|
|
1227
|
+
|
|
1228
|
+
// 确保scripts对象存在
|
|
1229
|
+
if (!packageJson.scripts) {
|
|
1230
|
+
packageJson.scripts = {};
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// 检查脚本是否已存在
|
|
1234
|
+
if (packageJson.scripts[scriptName]) {
|
|
1235
|
+
return res.status(400).json({
|
|
1236
|
+
success: false,
|
|
1237
|
+
error: `脚本 "${scriptName}" 已存在`
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// 添加脚本
|
|
1242
|
+
packageJson.scripts[scriptName] = scriptCommand;
|
|
1243
|
+
|
|
1244
|
+
// 写回文件(保持格式化)
|
|
1245
|
+
fsSync.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf8');
|
|
1246
|
+
|
|
1247
|
+
logger.info(`已添加npm脚本: ${scriptName} = ${scriptCommand} (${packagePath})`);
|
|
1248
|
+
|
|
1249
|
+
res.json({
|
|
1250
|
+
success: true,
|
|
1251
|
+
scriptName,
|
|
1252
|
+
scriptCommand
|
|
1253
|
+
});
|
|
1254
|
+
} catch (error) {
|
|
1255
|
+
logger.error('添加npm脚本失败:', error);
|
|
1256
|
+
res.status(500).json({
|
|
1257
|
+
success: false,
|
|
1258
|
+
error: `添加npm脚本失败: ${error.message}`
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
// API: 更新npm脚本
|
|
1264
|
+
app.post('/api/update-npm-script', async (req, res) => {
|
|
1265
|
+
try {
|
|
1266
|
+
const { packagePath, scriptName, scriptCommand, oldScriptName } = req.body;
|
|
1267
|
+
|
|
1268
|
+
if (!packagePath || !scriptName || !scriptCommand) {
|
|
1269
|
+
return res.status(400).json({
|
|
1270
|
+
success: false,
|
|
1271
|
+
error: '缺少必要参数: packagePath, scriptName, scriptCommand'
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// 确保路径指向package.json文件
|
|
1276
|
+
let packageJsonPath = path.resolve(packagePath);
|
|
1277
|
+
if (fsSync.existsSync(packageJsonPath) && fsSync.statSync(packageJsonPath).isDirectory()) {
|
|
1278
|
+
packageJsonPath = path.join(packageJsonPath, 'package.json');
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// 检查文件是否存在
|
|
1282
|
+
if (!fsSync.existsSync(packageJsonPath)) {
|
|
1283
|
+
return res.status(404).json({
|
|
1284
|
+
success: false,
|
|
1285
|
+
error: '找不到package.json文件'
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// 读取package.json
|
|
1290
|
+
const packageJson = JSON.parse(fsSync.readFileSync(packageJsonPath, 'utf8'));
|
|
1291
|
+
|
|
1292
|
+
// 确保scripts对象存在
|
|
1293
|
+
if (!packageJson.scripts) {
|
|
1294
|
+
packageJson.scripts = {};
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// 如果改了脚本名称,删除旧的
|
|
1298
|
+
if (oldScriptName && oldScriptName !== scriptName) {
|
|
1299
|
+
delete packageJson.scripts[oldScriptName];
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// 更新脚本
|
|
1303
|
+
packageJson.scripts[scriptName] = scriptCommand;
|
|
1304
|
+
|
|
1305
|
+
// 写回文件
|
|
1306
|
+
fsSync.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf8');
|
|
1307
|
+
|
|
1308
|
+
logger.info(`已更新npm脚本: ${scriptName} = ${scriptCommand} (${packagePath})`);
|
|
1309
|
+
|
|
1310
|
+
res.json({
|
|
1311
|
+
success: true,
|
|
1312
|
+
scriptName,
|
|
1313
|
+
scriptCommand
|
|
1314
|
+
});
|
|
1315
|
+
} catch (error) {
|
|
1316
|
+
logger.error('更新npm脚本失败:', error);
|
|
1317
|
+
res.status(500).json({
|
|
1318
|
+
success: false,
|
|
1319
|
+
error: `更新npm脚本失败: ${error.message}`
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
// API: 删除npm脚本
|
|
1325
|
+
app.post('/api/delete-npm-script', async (req, res) => {
|
|
1326
|
+
try {
|
|
1327
|
+
const { packagePath, scriptName } = req.body;
|
|
1328
|
+
|
|
1329
|
+
if (!packagePath || !scriptName) {
|
|
1330
|
+
return res.status(400).json({
|
|
1331
|
+
success: false,
|
|
1332
|
+
error: '缺少必要参数: packagePath, scriptName'
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// 确保路径指向package.json文件
|
|
1337
|
+
let packageJsonPath = path.resolve(packagePath);
|
|
1338
|
+
if (fsSync.existsSync(packageJsonPath) && fsSync.statSync(packageJsonPath).isDirectory()) {
|
|
1339
|
+
packageJsonPath = path.join(packageJsonPath, 'package.json');
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// 检查文件是否存在
|
|
1343
|
+
if (!fsSync.existsSync(packageJsonPath)) {
|
|
1344
|
+
return res.status(404).json({
|
|
1345
|
+
success: false,
|
|
1346
|
+
error: '找不到package.json文件'
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// 读取package.json
|
|
1351
|
+
const packageJson = JSON.parse(fsSync.readFileSync(packageJsonPath, 'utf8'));
|
|
1352
|
+
|
|
1353
|
+
// 检查scripts对象和脚本是否存在
|
|
1354
|
+
if (!packageJson.scripts || !packageJson.scripts[scriptName]) {
|
|
1355
|
+
return res.status(404).json({
|
|
1356
|
+
success: false,
|
|
1357
|
+
error: `脚本 "${scriptName}" 不存在`
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// 删除脚本
|
|
1362
|
+
delete packageJson.scripts[scriptName];
|
|
1363
|
+
|
|
1364
|
+
// 写回文件
|
|
1365
|
+
fsSync.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf8');
|
|
1366
|
+
|
|
1367
|
+
logger.info(`已删除npm脚本: ${scriptName} (${packagePath})`);
|
|
1368
|
+
|
|
1369
|
+
res.json({
|
|
1370
|
+
success: true,
|
|
1371
|
+
scriptName
|
|
1372
|
+
});
|
|
1373
|
+
} catch (error) {
|
|
1374
|
+
logger.error('删除npm脚本失败:', error);
|
|
1375
|
+
res.status(500).json({
|
|
1376
|
+
success: false,
|
|
1377
|
+
error: `删除npm脚本失败: ${error.message}`
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
});
|
|
1381
|
+
|
|
1382
|
+
// API: 置顶npm脚本
|
|
1383
|
+
app.post('/api/pin-npm-script', async (req, res) => {
|
|
1384
|
+
try {
|
|
1385
|
+
const { packagePath, scriptName } = req.body;
|
|
1386
|
+
|
|
1387
|
+
if (!packagePath || !scriptName) {
|
|
1388
|
+
return res.status(400).json({
|
|
1389
|
+
success: false,
|
|
1390
|
+
error: '缺少必要参数: packagePath, scriptName'
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// 确保路径指向package.json文件
|
|
1395
|
+
let packageJsonPath = path.resolve(packagePath);
|
|
1396
|
+
if (fsSync.existsSync(packageJsonPath) && fsSync.statSync(packageJsonPath).isDirectory()) {
|
|
1397
|
+
packageJsonPath = path.join(packageJsonPath, 'package.json');
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
// 检查文件是否存在
|
|
1401
|
+
if (!fsSync.existsSync(packageJsonPath)) {
|
|
1402
|
+
return res.status(404).json({
|
|
1403
|
+
success: false,
|
|
1404
|
+
error: '找不到package.json文件'
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// 读取package.json
|
|
1409
|
+
const packageJson = JSON.parse(fsSync.readFileSync(packageJsonPath, 'utf8'));
|
|
1410
|
+
|
|
1411
|
+
// 检查scripts对象和脚本是否存在
|
|
1412
|
+
if (!packageJson.scripts || !packageJson.scripts[scriptName]) {
|
|
1413
|
+
return res.status(404).json({
|
|
1414
|
+
success: false,
|
|
1415
|
+
error: `脚本 "${scriptName}" 不存在`
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// 保存要置顶的脚本内容
|
|
1420
|
+
const scriptCommand = packageJson.scripts[scriptName];
|
|
1421
|
+
|
|
1422
|
+
// 删除该脚本
|
|
1423
|
+
delete packageJson.scripts[scriptName];
|
|
1424
|
+
|
|
1425
|
+
// 创建新的scripts对象,将置顶脚本放在最前面
|
|
1426
|
+
const newScripts = {
|
|
1427
|
+
[scriptName]: scriptCommand,
|
|
1428
|
+
...packageJson.scripts
|
|
1429
|
+
};
|
|
1430
|
+
|
|
1431
|
+
packageJson.scripts = newScripts;
|
|
1432
|
+
|
|
1433
|
+
// 写回文件
|
|
1434
|
+
fsSync.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf8');
|
|
1435
|
+
|
|
1436
|
+
logger.info(`已置顶npm脚本: ${scriptName} (${packagePath})`);
|
|
1437
|
+
|
|
1438
|
+
res.json({
|
|
1439
|
+
success: true,
|
|
1440
|
+
scriptName
|
|
1441
|
+
});
|
|
1442
|
+
} catch (error) {
|
|
1443
|
+
logger.error('置顶npm脚本失败:', error);
|
|
1444
|
+
res.status(500).json({
|
|
1445
|
+
success: false,
|
|
1446
|
+
error: `置顶npm脚本失败: ${error.message}`
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
});
|
|
1450
|
+
}
|