zen-gitsync 2.11.39 → 2.12.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -21
- package/README.md +695 -695
- package/index.js +25 -11
- package/package.json +2 -2
- package/scripts/convert-colors-to-vars.cjs +286 -272
- package/scripts/convert-fontsize-to-vars.cjs +221 -207
- package/scripts/convert-spacing-to-vars.cjs +256 -242
- package/scripts/convert-to-standard-vars.cjs +282 -268
- package/scripts/release.js +599 -585
- package/src/config.js +350 -336
- package/src/gitCommit.js +455 -440
- package/src/ui/public/assets/EditorView-CbqSI9nw.css +1 -0
- package/src/ui/public/assets/EditorView-GS5cmh99.js +21 -0
- package/src/ui/public/assets/SourceMapView-DyMK80hS.css +1 -0
- package/src/ui/public/assets/SourceMapView-_YRtzmZZ.js +3 -0
- package/src/ui/public/assets/index-ML5Y-5lO.css +1 -0
- package/src/ui/public/assets/index-yky0Sd13.js +73 -0
- package/src/ui/public/assets/{ts.worker-Dth06zuC.js → ts.worker-METxwbDZ.js} +1 -16
- package/src/ui/public/assets/{vendor-B1T2uxYO.js → vendor-DITsiaGj.js} +294 -287
- package/src/ui/public/assets/vendor-q83wvJns.css +1 -0
- package/src/ui/public/index.html +4 -4
- package/src/ui/server/.claude/codediff.txt +6 -0
- package/src/ui/server/index.js +410 -396
- package/src/ui/server/middleware/requestLogger.js +51 -37
- package/src/ui/server/routes/branchStatus.js +101 -87
- package/src/ui/server/routes/code.js +110 -96
- package/src/ui/server/routes/codeAnalysis.js +995 -981
- package/src/ui/server/routes/config.js +1172 -1158
- package/src/ui/server/routes/exec.js +272 -258
- package/src/ui/server/routes/fileOpen.js +279 -265
- package/src/ui/server/routes/fs.js +701 -699
- package/src/ui/server/routes/git/diff.js +352 -338
- package/src/ui/server/routes/git/diffUtils.js +128 -114
- package/src/ui/server/routes/git/stash.js +552 -538
- package/src/ui/server/routes/git/tags.js +172 -158
- package/src/ui/server/routes/git.js +190 -176
- package/src/ui/server/routes/gitOps.js +1179 -1165
- package/src/ui/server/routes/instances.js +38 -24
- package/src/ui/server/routes/npm.js +1023 -1009
- package/src/ui/server/routes/process.js +82 -68
- package/src/ui/server/routes/status.js +67 -53
- package/src/ui/server/routes/terminal.js +319 -305
- package/src/ui/server/socket/registerUiSocketHandlers.js +226 -212
- package/src/ui/server/utils/createSavePortToFile.js +46 -32
- package/src/ui/server/utils/instanceRegistry.js +270 -256
- package/src/ui/server/utils/pathGuard.js +155 -0
- package/src/ui/server/utils/pathGuard.test.js +138 -0
- package/src/ui/server/utils/randomStartPort.js +51 -37
- package/src/ui/server/utils/startServerOnAvailablePort.js +101 -87
- package/src/utils/index.js +1058 -1044
- package/src/ui/public/assets/devopicons-QN4QXivI.woff2 +0 -0
- package/src/ui/public/assets/file-icons-C0jOugUK.woff2 +0 -0
- package/src/ui/public/assets/fontawesome-B-jkhYfk.woff2 +0 -0
- package/src/ui/public/assets/index-BvVl-092.js +0 -95
- package/src/ui/public/assets/index-DXO3Lvqi.css +0 -1
- package/src/ui/public/assets/mfixx-CpAhKOZz.woff2 +0 -0
- package/src/ui/public/assets/octicons-CaZ_fok2.woff2 +0 -0
- package/src/ui/public/assets/vendor-hOO_r_AU.css +0 -1
|
@@ -0,0 +1,155 @@
|
|
|
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
|
+
/**
|
|
16
|
+
* 路径越界检查中间件 / 工具
|
|
17
|
+
*
|
|
18
|
+
* 用法:
|
|
19
|
+
* // 1. 中间件模式:自动从 req.query.path 提取并校验
|
|
20
|
+
* router.get('/api/foo', pathGuard('path'), handler)
|
|
21
|
+
*
|
|
22
|
+
* // 2. 工具函数:在 handler 里手动校验
|
|
23
|
+
* const safePath = ensureWithinCwd(userPath, cwd)
|
|
24
|
+
*
|
|
25
|
+
* 防护:
|
|
26
|
+
* - 解析 `..`、相对路径
|
|
27
|
+
* - 拒绝以 `..` 开头或解析后是绝对路径的相对路径
|
|
28
|
+
* - Windows 大小写不敏感
|
|
29
|
+
* - 真实路径 (realpath) 防符号链接
|
|
30
|
+
*
|
|
31
|
+
* @typedef {Object} PathGuardOptions
|
|
32
|
+
* @property {string} field - 从 req.query / req.body 取的字段名(默认 'path')
|
|
33
|
+
* @property {boolean} [allowMissing] - 字段缺失时是否通过(默认 false)
|
|
34
|
+
* @property {boolean} [realpath] - 是否用 fs.realpath 进一步防符号链接(默认 false)
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import fs from 'fs/promises'
|
|
38
|
+
import path from 'path'
|
|
39
|
+
|
|
40
|
+
const isWindows = process.platform === 'win32'
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 把 user 输入路径解析成"在 cwd 内的绝对路径",越界时返回 null
|
|
44
|
+
*
|
|
45
|
+
* @param {string} input - 用户提供的路径(相对 / 绝对 / 含 .. / 符号链接)
|
|
46
|
+
* @param {string} cwd - 允许的根目录(当前项目目录)
|
|
47
|
+
* @param {{ realpath?: boolean }} [opts]
|
|
48
|
+
* @returns {Promise<{ safePath: string, realPath: string | null } | null>}
|
|
49
|
+
* - null 表示越界
|
|
50
|
+
* - safePath: 用于后续 path.resolve 等操作的绝对路径
|
|
51
|
+
* - realPath: realpath 结果(如果 opts.realpath=true 且文件存在)
|
|
52
|
+
*/
|
|
53
|
+
export async function ensureWithinCwd(input, cwd, opts = {}) {
|
|
54
|
+
if (typeof input !== 'string' || !input) return null
|
|
55
|
+
if (typeof cwd !== 'string' || !cwd) return null
|
|
56
|
+
|
|
57
|
+
let resolved
|
|
58
|
+
try {
|
|
59
|
+
resolved = path.resolve(cwd, input)
|
|
60
|
+
} catch {
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// path.relative 返回以 .. 开头的字符串代表越界(其它平台绝对路径返回绝对路径本身)
|
|
65
|
+
const rel = path.relative(cwd, resolved)
|
|
66
|
+
if (rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel))) {
|
|
67
|
+
// 在 cwd 内(相等或子路径)
|
|
68
|
+
} else {
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Windows 大小写不敏感:把 cwd 和 resolved 都转小写再比一次,确保 \ 不会绕过
|
|
73
|
+
if (isWindows) {
|
|
74
|
+
const lowerCwd = cwd.toLowerCase()
|
|
75
|
+
const lowerResolved = resolved.toLowerCase()
|
|
76
|
+
if (!lowerResolved.startsWith(lowerCwd)) {
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let realPath = null
|
|
82
|
+
if (opts.realpath) {
|
|
83
|
+
try {
|
|
84
|
+
realPath = await fs.realpath(resolved)
|
|
85
|
+
// realpath 后再校验一次(符号链接可能指向 cwd 之外)
|
|
86
|
+
const relReal = path.relative(cwd, realPath)
|
|
87
|
+
const isOutside = relReal === '..' || relReal.startsWith('..' + path.sep) || path.isAbsolute(relReal)
|
|
88
|
+
if (isOutside) return null
|
|
89
|
+
if (isWindows && !realPath.toLowerCase().startsWith(cwd.toLowerCase())) return null
|
|
90
|
+
} catch {
|
|
91
|
+
// 文件不存在 / 无权限:保持 null,让调用者决定
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { safePath: resolved, realPath }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Express 中间件工厂
|
|
100
|
+
*
|
|
101
|
+
* @param {string} cwd - 当前项目根
|
|
102
|
+
* @param {string | string[]} [fields] - 要校验的字段名(默认 'path',支持数组多字段)
|
|
103
|
+
* @param {{ realpath?: boolean }} [opts]
|
|
104
|
+
*/
|
|
105
|
+
export function pathGuard(cwd, fields = 'path', opts = {}) {
|
|
106
|
+
const fieldList = Array.isArray(fields) ? fields : [fields]
|
|
107
|
+
return async (req, res, next) => {
|
|
108
|
+
try {
|
|
109
|
+
for (const field of fieldList) {
|
|
110
|
+
const raw = req.query?.[field] ?? req.body?.[field]
|
|
111
|
+
if (raw === undefined || raw === null || raw === '') {
|
|
112
|
+
if (opts.allowMissing) continue
|
|
113
|
+
return res.status(400).json({ success: false, error: `缺少 ${field} 参数` })
|
|
114
|
+
}
|
|
115
|
+
const result = await ensureWithinCwd(String(raw), cwd, opts)
|
|
116
|
+
if (!result) {
|
|
117
|
+
return res.status(403).json({ success: false, error: `禁止访问工作目录以外的文件: ${raw}` })
|
|
118
|
+
}
|
|
119
|
+
// 把校验后的安全路径挂到 res.locals,handler 拿 res.locals.safePath 用
|
|
120
|
+
res.locals.safePath = result.safePath
|
|
121
|
+
res.locals.safeRealPath = result.realPath
|
|
122
|
+
}
|
|
123
|
+
next()
|
|
124
|
+
} catch (e) {
|
|
125
|
+
res.status(500).json({ success: false, error: e.message })
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 同步版本:只做路径解析校验,不做 realpath。用于不需要 fs 的纯路径场景
|
|
132
|
+
* @returns {string|null} 安全路径或 null
|
|
133
|
+
*/
|
|
134
|
+
export function ensureWithinCwdSync(input, cwd) {
|
|
135
|
+
if (typeof input !== 'string' || !input) return null
|
|
136
|
+
if (typeof cwd !== 'string' || !cwd) return null
|
|
137
|
+
|
|
138
|
+
let resolved
|
|
139
|
+
try {
|
|
140
|
+
resolved = path.resolve(cwd, input)
|
|
141
|
+
} catch {
|
|
142
|
+
return null
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const rel = path.relative(cwd, resolved)
|
|
146
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) return null
|
|
147
|
+
|
|
148
|
+
if (isWindows) {
|
|
149
|
+
const lowerCwd = cwd.toLowerCase()
|
|
150
|
+
const lowerResolved = resolved.toLowerCase()
|
|
151
|
+
if (!lowerResolved.startsWith(lowerCwd)) return null
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return resolved
|
|
155
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
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
|
+
// 路径越界检查单元测试(用 node:test 内置)
|
|
16
|
+
import { test } from 'node:test'
|
|
17
|
+
import assert from 'node:assert/strict'
|
|
18
|
+
import { ensureWithinCwd, ensureWithinCwdSync, pathGuard } from '../utils/pathGuard.js'
|
|
19
|
+
|
|
20
|
+
const cwd = process.platform === 'win32' ? 'E:\\project' : '/tmp/project'
|
|
21
|
+
|
|
22
|
+
test('合法路径在 cwd 内', async () => {
|
|
23
|
+
const r = await ensureWithinCwd('src/foo.ts', cwd)
|
|
24
|
+
assert.ok(r, '应返回结果')
|
|
25
|
+
assert.match(r.safePath, /[\\/]project[\\/]src[\\/]foo\.ts$/)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('合法绝对路径在 cwd 内', async () => {
|
|
29
|
+
const absInCwd = process.platform === 'win32' ? 'E:\\project\\sub' : '/tmp/project/sub'
|
|
30
|
+
const r = await ensureWithinCwd(absInCwd, cwd)
|
|
31
|
+
assert.ok(r, '应通过')
|
|
32
|
+
assert.equal(r.safePath, absInCwd)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('../ 父目录逃逸被拒绝', async () => {
|
|
36
|
+
const r = await ensureWithinCwd('../etc/passwd', cwd)
|
|
37
|
+
assert.equal(r, null, '../ 应该被拒')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('cwd 的同级目录(startsWith 假阳性)被拒绝', async () => {
|
|
41
|
+
const evil = process.platform === 'win32' ? 'E:\\project-evil' : '/tmp/project-evil'
|
|
42
|
+
const r = await ensureWithinCwd(evil, cwd)
|
|
43
|
+
assert.equal(r, null, '同名前缀目录应该被拒')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('绝对路径在 cwd 外被拒', async () => {
|
|
47
|
+
const outside = process.platform === 'win32' ? 'C:\\Windows\\System32' : '/etc/passwd'
|
|
48
|
+
const r = await ensureWithinCwd(outside, cwd)
|
|
49
|
+
assert.equal(r, null, '绝对外部路径应该被拒')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('空字符串 / null / undefined 被拒', async () => {
|
|
53
|
+
assert.equal(await ensureWithinCwd('', cwd), null)
|
|
54
|
+
assert.equal(await ensureWithinCwd(null, cwd), null)
|
|
55
|
+
assert.equal(await ensureWithinCwd(undefined, cwd), null)
|
|
56
|
+
assert.equal(await ensureWithinCwd(123, cwd), null)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('多重 ../ 也被拒', async () => {
|
|
60
|
+
const r = await ensureWithinCwd('../../../../etc/passwd', cwd)
|
|
61
|
+
assert.equal(r, null)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('相对 cwd 自身(.)允许', async () => {
|
|
65
|
+
const r = await ensureWithinCwd('.', cwd)
|
|
66
|
+
assert.ok(r, '点 应解析为 cwd 本身')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('Windows 反斜杠与正斜杠混用允许', async () => {
|
|
70
|
+
if (process.platform !== 'win32') return
|
|
71
|
+
const r = await ensureWithinCwd('src\\sub/foo.ts', cwd)
|
|
72
|
+
assert.ok(r)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('Windows 大小写不敏感', async () => {
|
|
76
|
+
if (process.platform !== 'win32') return
|
|
77
|
+
const r = await ensureWithinCwd('SRC/FOO.TS', 'E:\\Project')
|
|
78
|
+
assert.ok(r, 'Windows 下大小写不敏感应允许')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('sync 版本对 .. 同样拒绝', () => {
|
|
82
|
+
assert.equal(ensureWithinCwdSync('../foo', cwd), null)
|
|
83
|
+
const r = ensureWithinCwdSync('sub/file', cwd)
|
|
84
|
+
assert.ok(r)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('中间件: 合法 path 通过 + 挂到 res.locals.safePath', async () => {
|
|
88
|
+
let nextCalled = false
|
|
89
|
+
const req = { query: { path: 'src/foo.ts' } }
|
|
90
|
+
const res = {
|
|
91
|
+
locals: {},
|
|
92
|
+
status() { return this },
|
|
93
|
+
json() { return this },
|
|
94
|
+
}
|
|
95
|
+
await pathGuard(cwd)(req, res, () => { nextCalled = true })
|
|
96
|
+
assert.equal(nextCalled, true)
|
|
97
|
+
assert.match(res.locals.safePath, /[\\/]src[\\/]foo\.ts$/)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('中间件: 越界 path 返回 403', async () => {
|
|
101
|
+
let nextCalled = false
|
|
102
|
+
let statusCode = 0
|
|
103
|
+
let body = null
|
|
104
|
+
const req = { query: { path: '../etc/passwd' } }
|
|
105
|
+
const res = {
|
|
106
|
+
locals: {},
|
|
107
|
+
status(c) { statusCode = c; return this },
|
|
108
|
+
json(b) { body = b; return this },
|
|
109
|
+
}
|
|
110
|
+
await pathGuard(cwd)(req, res, () => { nextCalled = true })
|
|
111
|
+
assert.equal(nextCalled, false)
|
|
112
|
+
assert.equal(statusCode, 403)
|
|
113
|
+
assert.match(body.error, /禁止访问/)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('中间件: 缺字段返回 400', async () => {
|
|
117
|
+
let statusCode = 0
|
|
118
|
+
const req = { query: {} }
|
|
119
|
+
const res = {
|
|
120
|
+
locals: {},
|
|
121
|
+
status(c) { statusCode = c; return this },
|
|
122
|
+
json() { return this },
|
|
123
|
+
}
|
|
124
|
+
await pathGuard(cwd)(req, res, () => {})
|
|
125
|
+
assert.equal(statusCode, 400)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test('中间件: 多字段校验(rename 用)', async () => {
|
|
129
|
+
let nextCalled = false
|
|
130
|
+
const req = { query: { oldPath: 'src/a.ts', newPath: '../b.ts' } }
|
|
131
|
+
const res = {
|
|
132
|
+
locals: {},
|
|
133
|
+
status() { return this },
|
|
134
|
+
json() { return this },
|
|
135
|
+
}
|
|
136
|
+
await pathGuard(cwd, ['oldPath', 'newPath'])(req, res, () => { nextCalled = true })
|
|
137
|
+
assert.equal(nextCalled, false, 'newPath 越界应拒')
|
|
138
|
+
})
|
|
@@ -1,37 +1,51 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
+
// 随机起点端口选择
|
|
16
|
+
// 默认:避免与系统保留 / 常见服务端口重叠,在较宽的"用户态"范围里随机挑一个起点
|
|
17
|
+
// 然后由 startServerOnAvailablePort 在该起点基础上顺序往上扫描 EADDRINUSE
|
|
18
|
+
// 覆盖:环境变量 PORT 强制使用固定端口(向后兼容 + 便于书签/调试)
|
|
19
|
+
|
|
20
|
+
const DEFAULT_MIN = 4000;
|
|
21
|
+
const DEFAULT_MAX = 6000; // 2000 端口的池子;配合 maxTries 100 实际能覆盖 (max - min) 区间
|
|
22
|
+
|
|
23
|
+
function pickRandomInt(min, max) {
|
|
24
|
+
// 含 min,不含 max(与 Math.random 习惯一致)
|
|
25
|
+
return Math.floor(Math.random() * (max - min)) + min;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 解析本次启动应使用的起始端口
|
|
30
|
+
* @param {object} [opts]
|
|
31
|
+
* @param {number} [opts.min] 随机范围下界(默认 4000)
|
|
32
|
+
* @param {number} [opts.max] 随机范围上界(默认 6000)
|
|
33
|
+
* @returns {{ startPort: number, source: 'env'|'random', min: number, max: number }}
|
|
34
|
+
*/
|
|
35
|
+
export function resolveStartPort({ min = DEFAULT_MIN, max = DEFAULT_MAX } = {}) {
|
|
36
|
+
// 1) 环境变量优先:用户显式指定 > 一切
|
|
37
|
+
const envPort = Number(process.env.PORT);
|
|
38
|
+
if (Number.isInteger(envPort) && envPort > 0 && envPort < 65536) {
|
|
39
|
+
return { startPort: envPort, source: 'env', min, max };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 2) 兜底:参数范围非法就回退到 [4000, 6000)
|
|
43
|
+
if (!Number.isInteger(min) || !Number.isInteger(max) || min < 1 || max > 65535 || min >= max) {
|
|
44
|
+
min = DEFAULT_MIN;
|
|
45
|
+
max = DEFAULT_MAX;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 3) 随机挑一个起点
|
|
49
|
+
const startPort = pickRandomInt(min, max);
|
|
50
|
+
return { startPort, source: 'random', min, max };
|
|
51
|
+
}
|
|
@@ -1,87 +1,101 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
+
export async function startServerOnAvailablePort({
|
|
16
|
+
httpServer,
|
|
17
|
+
startPort,
|
|
18
|
+
chalk,
|
|
19
|
+
open,
|
|
20
|
+
noOpen,
|
|
21
|
+
isGitRepo,
|
|
22
|
+
savePortToFile,
|
|
23
|
+
maxTries = 100,
|
|
24
|
+
callbackExecutedRef
|
|
25
|
+
}) {
|
|
26
|
+
let currentPort = startPort;
|
|
27
|
+
const maxPort = startPort + maxTries;
|
|
28
|
+
const getCallbackExecuted = () => {
|
|
29
|
+
if (callbackExecutedRef && typeof callbackExecutedRef === 'object' && 'value' in callbackExecutedRef) {
|
|
30
|
+
return Boolean(callbackExecutedRef.value);
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const setCallbackExecuted = (value) => {
|
|
36
|
+
if (callbackExecutedRef && typeof callbackExecutedRef === 'object' && 'value' in callbackExecutedRef) {
|
|
37
|
+
callbackExecutedRef.value = Boolean(value);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
while (currentPort < maxPort) {
|
|
42
|
+
try {
|
|
43
|
+
if (currentPort > startPort) {
|
|
44
|
+
await new Promise(resolve => setTimeout(resolve, 800));
|
|
45
|
+
console.log(`尝试端口 ${currentPort}...`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await new Promise((resolve, reject) => {
|
|
49
|
+
const errorHandler = (err) => {
|
|
50
|
+
httpServer.removeListener('error', errorHandler);
|
|
51
|
+
reject(err);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
httpServer.once('error', errorHandler);
|
|
55
|
+
|
|
56
|
+
httpServer.listen(currentPort, () => {
|
|
57
|
+
if (getCallbackExecuted()) return;
|
|
58
|
+
setCallbackExecuted(true);
|
|
59
|
+
|
|
60
|
+
httpServer.removeListener('error', errorHandler);
|
|
61
|
+
|
|
62
|
+
console.log(chalk.green('======================================'));
|
|
63
|
+
console.log(chalk.green(` Zen GitSync 服务器已启动`));
|
|
64
|
+
console.log(chalk.green(` 访问地址: http://localhost:${currentPort}`));
|
|
65
|
+
console.log(chalk.green(` 启动时间: ${new Date().toLocaleString()}`));
|
|
66
|
+
|
|
67
|
+
if (isGitRepo) {
|
|
68
|
+
console.log(chalk.green(` 当前目录是Git仓库`));
|
|
69
|
+
} else {
|
|
70
|
+
console.log(chalk.yellow(` 当前目录不是Git仓库,文件监控未启动`));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log(chalk.green('======================================'));
|
|
74
|
+
|
|
75
|
+
savePortToFile(currentPort);
|
|
76
|
+
|
|
77
|
+
if (!noOpen) {
|
|
78
|
+
setTimeout(() => {
|
|
79
|
+
open(`http://localhost:${currentPort}`);
|
|
80
|
+
}, 0);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
resolve();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return currentPort;
|
|
88
|
+
} catch (err) {
|
|
89
|
+
if (err.code === 'EADDRINUSE') {
|
|
90
|
+
console.log(`端口 ${currentPort} 被占用,尝试下一个端口...`);
|
|
91
|
+
currentPort++;
|
|
92
|
+
} else {
|
|
93
|
+
console.error('启动服务器失败:', err);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.error(`无法找到可用端口 (尝试范围: ${startPort}-${maxPort - 1})`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|