@wangzhanpeng996/wzp 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -54
- package/bin/wzp.js +174 -180
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,15 +11,6 @@
|
|
|
11
11
|
|
|
12
12
|
## 安装
|
|
13
13
|
|
|
14
|
-
本机开发调试:
|
|
15
|
-
|
|
16
|
-
```powershell
|
|
17
|
-
npm link
|
|
18
|
-
wzp --help
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
发布到 npm 后:
|
|
22
|
-
|
|
23
14
|
```powershell
|
|
24
15
|
npm i -g @wangzhanpeng996/wzp
|
|
25
16
|
```
|
|
@@ -32,41 +23,40 @@ npm i -g @wangzhanpeng996/wzp
|
|
|
32
23
|
$env:GITEE_TOKEN="你的Gitee访问令牌"
|
|
33
24
|
```
|
|
34
25
|
|
|
35
|
-
|
|
26
|
+
基本用法:
|
|
36
27
|
|
|
37
28
|
```powershell
|
|
38
|
-
wzp gitee-create
|
|
29
|
+
wzp gitee-create <源仓库地址> [新仓库名] [源分支名]
|
|
39
30
|
```
|
|
40
31
|
|
|
41
|
-
|
|
32
|
+
示例:
|
|
42
33
|
|
|
43
34
|
```powershell
|
|
44
|
-
wzp gitee-create
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
--description "我的新项目" `
|
|
48
|
-
--private `
|
|
49
|
-
--no-push
|
|
35
|
+
wzp gitee-create https://gitee.com/wzplxt/web1.git
|
|
36
|
+
wzp gitee-create https://gitee.com/wzplxt/web1.git recruitment
|
|
37
|
+
wzp gitee-create https://gitee.com/wzplxt/web1.git recruitment Paddy
|
|
50
38
|
```
|
|
51
39
|
|
|
52
|
-
|
|
40
|
+
含义:
|
|
53
41
|
|
|
54
|
-
1
|
|
55
|
-
2
|
|
56
|
-
|
|
57
|
-
4. 删除克隆下来的原 `.git`。
|
|
58
|
-
5. 重新执行 `git init`。
|
|
59
|
-
6. 把 `origin` 指向新 Gitee 仓库,默认优先使用 HTTPS 地址。
|
|
60
|
-
7. 执行 `git add .` 和一次初始化提交。
|
|
61
|
-
8. 默认继续推送到新 Gitee 仓库;如果加了 `--no-push`,只在本地准备好。
|
|
42
|
+
- 第 1 个参数是源仓库地址,必填。
|
|
43
|
+
- 第 2 个参数是新仓库名,可选;不填时从源仓库地址自动提取,例如 `web1.git` 会提取为 `web1`。
|
|
44
|
+
- 第 3 个参数是源分支名,可选;不填时克隆源仓库默认分支。
|
|
62
45
|
|
|
63
|
-
|
|
46
|
+
可选参数:
|
|
64
47
|
|
|
65
48
|
```powershell
|
|
66
|
-
|
|
49
|
+
--description "仓库简介"
|
|
50
|
+
--private
|
|
51
|
+
--no-push
|
|
67
52
|
```
|
|
68
53
|
|
|
69
|
-
|
|
54
|
+
默认行为:
|
|
55
|
+
|
|
56
|
+
- 默认创建公开仓库。
|
|
57
|
+
- 默认自动推送到新 Gitee 仓库。
|
|
58
|
+
- 如果加 `--private`,创建私有仓库。
|
|
59
|
+
- 如果加 `--no-push`,只在本地准备好,不推送。
|
|
70
60
|
|
|
71
61
|
## kill
|
|
72
62
|
|
|
@@ -82,22 +72,11 @@ Windows 下会查找正在监听该 TCP 端口的进程,然后执行:
|
|
|
82
72
|
taskkill /PID <进程ID> /F
|
|
83
73
|
```
|
|
84
74
|
|
|
85
|
-
如果没有进程监听该端口,会直接提示无需处理。
|
|
86
|
-
|
|
87
75
|
## 发布
|
|
88
76
|
|
|
89
|
-
如果这是第一次发布 `wzp` 这个新包名:
|
|
90
|
-
|
|
91
|
-
```powershell
|
|
92
|
-
npm login
|
|
93
|
-
npm publish --access=public --otp=你的6位验证码
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
后续更新:
|
|
97
|
-
|
|
98
77
|
```powershell
|
|
99
78
|
npm version patch
|
|
100
|
-
npm publish --access=public
|
|
79
|
+
npm publish --access=public
|
|
101
80
|
```
|
|
102
81
|
|
|
103
82
|
别人更新:
|
|
@@ -105,15 +84,3 @@ npm publish --access=public --otp=你的6位验证码
|
|
|
105
84
|
```powershell
|
|
106
85
|
npm i -g @wangzhanpeng996/wzp@latest
|
|
107
86
|
```
|
|
108
|
-
|
|
109
|
-
## 继续扩展新功能
|
|
110
|
-
|
|
111
|
-
以后新增功能时,在 `bin/wzp.js` 里增加新的子命令分支即可,例如:
|
|
112
|
-
|
|
113
|
-
```text
|
|
114
|
-
wzp md-init
|
|
115
|
-
wzp env-check
|
|
116
|
-
wzp config
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
推荐把 `wzp` 作为总入口,把具体能力都放在子命令里。
|
package/bin/wzp.js
CHANGED
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
5
|
-
import { mkdir, readdir, rm
|
|
6
|
-
import {
|
|
5
|
+
import { mkdir, readdir, rm } from "node:fs/promises";
|
|
6
|
+
import { dirname, isAbsolute, resolve } from "node:path";
|
|
7
7
|
import process from "node:process";
|
|
8
|
-
import readline from "node:readline/promises";
|
|
9
8
|
|
|
10
9
|
const globalHelp = `
|
|
11
10
|
用法:
|
|
@@ -18,31 +17,34 @@ const globalHelp = `
|
|
|
18
17
|
kill-port kill 的兼容别名
|
|
19
18
|
|
|
20
19
|
示例:
|
|
21
|
-
wzp gitee-create https://gitee.com/wzplxt/web1.git
|
|
22
|
-
wzp gitee-create
|
|
20
|
+
wzp gitee-create https://gitee.com/wzplxt/web1.git
|
|
21
|
+
wzp gitee-create https://gitee.com/wzplxt/web1.git recruitment
|
|
22
|
+
wzp gitee-create https://gitee.com/wzplxt/web1.git recruitment Paddy
|
|
23
23
|
wzp kill 8080
|
|
24
|
-
|
|
25
|
-
查看子命令帮助:
|
|
26
|
-
wzp gitee-create --help
|
|
27
|
-
wzp kill --help
|
|
28
24
|
`;
|
|
29
25
|
|
|
30
26
|
const giteeCreateHelp = `
|
|
31
27
|
用法:
|
|
32
|
-
wzp gitee-create <源仓库地址>
|
|
33
|
-
|
|
28
|
+
wzp gitee-create <源仓库地址> [新仓库名] [源分支名]
|
|
29
|
+
|
|
30
|
+
参数:
|
|
31
|
+
源仓库地址 必填。要克隆的原项目地址
|
|
32
|
+
新仓库名 可选。不填时从源仓库地址里自动提取
|
|
33
|
+
源分支名 可选。不填时克隆源仓库默认分支
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
--source <url> 要克隆的原项目地址,也可以作为第 1 个位置参数
|
|
37
|
-
--repo <name> 新 Gitee 仓库路径名,也会作为当前目录下的新项目文件夹名
|
|
35
|
+
选项:
|
|
38
36
|
--description <text> Gitee 仓库简介
|
|
39
37
|
--private 创建私有 Gitee 仓库
|
|
40
38
|
--no-push 初始化提交后不推送到 origin
|
|
41
39
|
-h, --help 显示帮助
|
|
42
40
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
示例:
|
|
42
|
+
wzp gitee-create https://gitee.com/wzplxt/web1.git
|
|
43
|
+
wzp gitee-create https://gitee.com/wzplxt/web1.git recruitment
|
|
44
|
+
wzp gitee-create https://gitee.com/wzplxt/web1.git recruitment Paddy
|
|
45
|
+
|
|
46
|
+
认证:
|
|
47
|
+
需要先设置环境变量 GITEE_TOKEN。没有令牌或令牌无效会直接退出。
|
|
46
48
|
`;
|
|
47
49
|
|
|
48
50
|
const killHelp = `
|
|
@@ -52,10 +54,6 @@ const killHelp = `
|
|
|
52
54
|
|
|
53
55
|
示例:
|
|
54
56
|
wzp kill 8080
|
|
55
|
-
|
|
56
|
-
说明:
|
|
57
|
-
Windows 下会查找正在监听该 TCP 端口的进程,并执行 taskkill /F。
|
|
58
|
-
请只对你确认可以关闭的本地开发服务使用。
|
|
59
57
|
`;
|
|
60
58
|
|
|
61
59
|
main().catch((error) => {
|
|
@@ -82,168 +80,142 @@ async function main() {
|
|
|
82
80
|
return;
|
|
83
81
|
}
|
|
84
82
|
|
|
85
|
-
throw new Error(`未知命令: ${command}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async function runKillPort(args) {
|
|
89
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
90
|
-
console.log(killHelp.trim());
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const port = args.find((arg) => !arg.startsWith("-"));
|
|
95
|
-
if (!port || !/^\d+$/.test(port)) {
|
|
96
|
-
throw new Error("缺少端口号,例如: wzp kill 8080");
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const portNumber = Number(port);
|
|
100
|
-
if (portNumber < 1 || portNumber > 65535) {
|
|
101
|
-
throw new Error("端口号必须在 1 到 65535 之间");
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (process.platform === "win32") {
|
|
105
|
-
await killPortOnWindows(portNumber);
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
await killPortOnUnix(portNumber);
|
|
83
|
+
throw new Error(`未知命令: ${command}`);
|
|
110
84
|
}
|
|
111
85
|
|
|
112
86
|
async function runGiteeCreate(args) {
|
|
113
|
-
const parsed =
|
|
87
|
+
const parsed = parseGiteeCreateArgs(args);
|
|
114
88
|
if (parsed.help) {
|
|
115
89
|
console.log(giteeCreateHelp.trim());
|
|
116
90
|
return;
|
|
117
91
|
}
|
|
118
92
|
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
93
|
+
const source = parsed.positionals[0];
|
|
94
|
+
if (!source) {
|
|
95
|
+
throw new Error("缺少源仓库地址,例如: wzp gitee-create https://gitee.com/wzplxt/web1.git");
|
|
96
|
+
}
|
|
123
97
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
console.log(`Gitee 令牌有效,当前用户: ${authUser.login || authUser.name || authUser.id}`);
|
|
98
|
+
const repo = parsed.positionals[1] || inferRepoName(source);
|
|
99
|
+
const branch = parsed.positionals[2];
|
|
100
|
+
const authUser = await validateGiteeToken(process.env.GITEE_TOKEN);
|
|
101
|
+
console.log(`Gitee 令牌有效,当前用户: ${authUser.login || authUser.name || authUser.id}`);
|
|
129
102
|
|
|
130
|
-
|
|
131
|
-
|
|
103
|
+
const target = resolve(process.cwd(), repo);
|
|
104
|
+
const push = !parsed.options.noPush;
|
|
132
105
|
|
|
133
|
-
|
|
106
|
+
await ensureTargetAvailable(target);
|
|
134
107
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
108
|
+
const remote = await createGiteeRepo({
|
|
109
|
+
owner: authUser.login,
|
|
110
|
+
repo,
|
|
111
|
+
name: repo,
|
|
112
|
+
description: parsed.options.description ?? "",
|
|
113
|
+
isPrivate: Boolean(parsed.options.private),
|
|
114
|
+
token: process.env.GITEE_TOKEN
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const cloneArgs = ["clone"];
|
|
118
|
+
if (branch) {
|
|
119
|
+
cloneArgs.push("--branch", branch, "--single-branch");
|
|
120
|
+
}
|
|
121
|
+
cloneArgs.push(source, target);
|
|
143
122
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
123
|
+
await run("git", cloneArgs);
|
|
124
|
+
await detachGitHistory(target);
|
|
125
|
+
await run("git", ["init"], { cwd: target });
|
|
126
|
+
await run("git", ["remote", "add", "origin", remote], { cwd: target });
|
|
127
|
+
await run("git", ["add", "."], { cwd: target });
|
|
128
|
+
await run("git", ["commit", "-m", "初始化项目"], { cwd: target, allowFailure: true });
|
|
150
129
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
130
|
+
if (push) {
|
|
131
|
+
await run("git", ["push", "-u", "origin", "HEAD"], { cwd: target });
|
|
132
|
+
}
|
|
154
133
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
134
|
+
console.log("\n完成");
|
|
135
|
+
console.log(`本地目录: ${target}`);
|
|
136
|
+
console.log(`新 remote: ${remote}`);
|
|
137
|
+
if (branch) {
|
|
138
|
+
console.log(`源分支: ${branch}`);
|
|
139
|
+
}
|
|
140
|
+
if (!push) {
|
|
141
|
+
console.log(`尚未推送。确认无误后可执行: git -C "${target}" push -u origin HEAD`);
|
|
163
142
|
}
|
|
164
143
|
}
|
|
165
144
|
|
|
166
|
-
function
|
|
145
|
+
function parseGiteeCreateArgs(args) {
|
|
167
146
|
const result = {
|
|
168
147
|
help: args.includes("--help") || args.includes("-h"),
|
|
169
|
-
options: {}
|
|
148
|
+
options: {},
|
|
149
|
+
positionals: []
|
|
170
150
|
};
|
|
171
151
|
|
|
172
|
-
const positionals = [];
|
|
173
152
|
for (let i = 0; i < args.length; i += 1) {
|
|
174
153
|
const arg = args[i];
|
|
175
154
|
if (arg === "--help" || arg === "-h") {
|
|
176
155
|
continue;
|
|
177
156
|
}
|
|
178
|
-
if (
|
|
179
|
-
|
|
157
|
+
if (arg === "--private") {
|
|
158
|
+
result.options.private = true;
|
|
180
159
|
continue;
|
|
181
160
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const key = toCamelCase(rawKey);
|
|
185
|
-
if (["private", "noPush"].includes(key)) {
|
|
186
|
-
result.options[key] = true;
|
|
161
|
+
if (arg === "--no-push") {
|
|
162
|
+
result.options.noPush = true;
|
|
187
163
|
continue;
|
|
188
164
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
if (inlineValue === undefined) {
|
|
165
|
+
if (arg === "--description") {
|
|
166
|
+
const value = args[i + 1];
|
|
167
|
+
if (!value || value.startsWith("--")) {
|
|
168
|
+
throw new Error("参数 --description 缺少值");
|
|
169
|
+
}
|
|
170
|
+
result.options.description = value;
|
|
196
171
|
i += 1;
|
|
172
|
+
continue;
|
|
197
173
|
}
|
|
174
|
+
if (arg.startsWith("--description=")) {
|
|
175
|
+
result.options.description = arg.slice("--description=".length);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (arg.startsWith("--")) {
|
|
179
|
+
throw new Error(`未知参数: ${arg}`);
|
|
180
|
+
}
|
|
181
|
+
result.positionals.push(arg);
|
|
198
182
|
}
|
|
199
183
|
|
|
200
|
-
result.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
184
|
+
if (result.positionals.length > 3) {
|
|
185
|
+
throw new Error("位置参数太多。正确格式: wzp gitee-create <源仓库地址> [新仓库名] [源分支名]");
|
|
186
|
+
}
|
|
204
187
|
|
|
205
|
-
|
|
206
|
-
return value.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
188
|
+
return result;
|
|
207
189
|
}
|
|
208
190
|
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
191
|
+
function inferRepoName(source) {
|
|
192
|
+
const cleaned = source
|
|
193
|
+
.replace(/[?#].*$/, "")
|
|
194
|
+
.replace(/\/+$/, "")
|
|
195
|
+
.replace(/\.git$/i, "");
|
|
196
|
+
const name = cleaned.split(/[/:\\]/).filter(Boolean).pop();
|
|
197
|
+
if (!name) {
|
|
198
|
+
throw new Error("无法从源仓库地址推断新仓库名,请手动传入新仓库名");
|
|
213
199
|
}
|
|
214
|
-
|
|
215
|
-
if (!answer.trim()) {
|
|
216
|
-
throw new Error(`缺少必填参数: ${label}`);
|
|
217
|
-
}
|
|
218
|
-
return answer.trim();
|
|
200
|
+
return name;
|
|
219
201
|
}
|
|
220
202
|
|
|
221
|
-
async function
|
|
222
|
-
if (!
|
|
223
|
-
throw new Error("
|
|
224
|
-
}
|
|
225
|
-
if (!existsSync(dirname(target))) {
|
|
226
|
-
await mkdir(dirname(target), { recursive: true });
|
|
227
|
-
}
|
|
228
|
-
if (!existsSync(target)) {
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
const entries = await readdir(target);
|
|
232
|
-
if (entries.length > 0) {
|
|
233
|
-
throw new Error(`目标目录已存在且不是空目录: ${target}`);
|
|
203
|
+
async function validateGiteeToken(token) {
|
|
204
|
+
if (!token) {
|
|
205
|
+
throw new Error("缺少 GITEE_TOKEN。请先在当前 PowerShell 会话执行: $env:GITEE_TOKEN=\"你的Gitee访问令牌\"");
|
|
234
206
|
}
|
|
235
|
-
}
|
|
236
207
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
await rm(gitDir, { recursive: true, force: true });
|
|
208
|
+
const url = new URL("https://gitee.com/api/v5/user");
|
|
209
|
+
url.searchParams.set("access_token", token);
|
|
210
|
+
const response = await fetch(url);
|
|
211
|
+
const payload = await response.json().catch(() => ({}));
|
|
212
|
+
|
|
213
|
+
if (!response.ok || !payload.id) {
|
|
214
|
+
const message = payload.message || payload.error || response.statusText;
|
|
215
|
+
throw new Error(`GITEE_TOKEN 无效或权限不足: ${message}`);
|
|
246
216
|
}
|
|
217
|
+
|
|
218
|
+
return payload;
|
|
247
219
|
}
|
|
248
220
|
|
|
249
221
|
async function createGiteeRepo({ owner, repo, name, description, isPrivate, token }) {
|
|
@@ -272,42 +244,54 @@ async function createGiteeRepo({ owner, repo, name, description, isPrivate, toke
|
|
|
272
244
|
return payload.html_url || payload.ssh_url || `https://gitee.com/${owner}/${repo}.git`;
|
|
273
245
|
}
|
|
274
246
|
|
|
275
|
-
async function
|
|
276
|
-
if (!
|
|
277
|
-
throw new Error("
|
|
247
|
+
async function ensureTargetAvailable(target) {
|
|
248
|
+
if (!isAbsolute(target)) {
|
|
249
|
+
throw new Error("本地目标目录必须能解析为绝对路径");
|
|
250
|
+
}
|
|
251
|
+
if (!existsSync(dirname(target))) {
|
|
252
|
+
await mkdir(dirname(target), { recursive: true });
|
|
253
|
+
}
|
|
254
|
+
if (!existsSync(target)) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const entries = await readdir(target);
|
|
258
|
+
if (entries.length > 0) {
|
|
259
|
+
throw new Error(`目标目录已存在且不是空目录: ${target}`);
|
|
278
260
|
}
|
|
261
|
+
}
|
|
279
262
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const
|
|
283
|
-
|
|
263
|
+
async function detachGitHistory(target) {
|
|
264
|
+
const gitDir = resolve(target, ".git");
|
|
265
|
+
const resolvedTarget = resolve(target);
|
|
266
|
+
if (!gitDir.startsWith(`${resolvedTarget}\\`) && gitDir !== resolve(resolvedTarget, ".git")) {
|
|
267
|
+
throw new Error(`拒绝删除异常路径: ${gitDir}`);
|
|
268
|
+
}
|
|
269
|
+
if (existsSync(gitDir)) {
|
|
270
|
+
await rm(gitDir, { recursive: true, force: true });
|
|
271
|
+
}
|
|
272
|
+
}
|
|
284
273
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
274
|
+
async function runKillPort(args) {
|
|
275
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
276
|
+
console.log(killHelp.trim());
|
|
277
|
+
return;
|
|
288
278
|
}
|
|
289
279
|
|
|
290
|
-
|
|
291
|
-
|
|
280
|
+
const port = args.find((arg) => !arg.startsWith("-"));
|
|
281
|
+
if (!port || !/^\d+$/.test(port)) {
|
|
282
|
+
throw new Error("缺少端口号,例如: wzp kill 8080");
|
|
283
|
+
}
|
|
292
284
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
cwd: options.cwd ?? process.cwd(),
|
|
298
|
-
stdio: "inherit",
|
|
299
|
-
shell: false
|
|
300
|
-
});
|
|
285
|
+
const portNumber = Number(port);
|
|
286
|
+
if (portNumber < 1 || portNumber > 65535) {
|
|
287
|
+
throw new Error("端口号必须在 1 到 65535 之间");
|
|
288
|
+
}
|
|
301
289
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
308
|
-
reject(new Error(`命令退出码 ${code}: ${command} ${args.join(" ")}`));
|
|
309
|
-
});
|
|
310
|
-
});
|
|
290
|
+
if (process.platform === "win32") {
|
|
291
|
+
await killPortOnWindows(portNumber);
|
|
292
|
+
} else {
|
|
293
|
+
await killPortOnUnix(portNumber);
|
|
294
|
+
}
|
|
311
295
|
}
|
|
312
296
|
|
|
313
297
|
async function killPortOnWindows(port) {
|
|
@@ -319,16 +303,11 @@ async function killPortOnWindows(port) {
|
|
|
319
303
|
if (!trimmed || !trimmed.includes("LISTENING")) {
|
|
320
304
|
continue;
|
|
321
305
|
}
|
|
322
|
-
|
|
323
306
|
const parts = trimmed.split(/\s+/);
|
|
324
307
|
const localAddress = parts[1];
|
|
325
308
|
const state = parts[3];
|
|
326
309
|
const pid = parts[4];
|
|
327
|
-
if (state
|
|
328
|
-
continue;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
if (addressUsesPort(localAddress, port)) {
|
|
310
|
+
if (state === "LISTENING" && pid && addressUsesPort(localAddress, port)) {
|
|
332
311
|
pids.add(pid);
|
|
333
312
|
}
|
|
334
313
|
}
|
|
@@ -342,16 +321,12 @@ async function killPortOnWindows(port) {
|
|
|
342
321
|
console.log(`释放端口 ${port}: 结束 PID ${pid}`);
|
|
343
322
|
await run("taskkill", ["/PID", pid, "/F"]);
|
|
344
323
|
}
|
|
345
|
-
|
|
346
324
|
console.log(`端口 ${port} 已释放。`);
|
|
347
325
|
}
|
|
348
326
|
|
|
349
327
|
async function killPortOnUnix(port) {
|
|
350
328
|
const output = await capture("lsof", ["-ti", `tcp:${port}`, "-sTCP:LISTEN"], { allowFailure: true });
|
|
351
|
-
const pids = output
|
|
352
|
-
.split(/\r?\n/)
|
|
353
|
-
.map((line) => line.trim())
|
|
354
|
-
.filter(Boolean);
|
|
329
|
+
const pids = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
355
330
|
|
|
356
331
|
if (pids.length === 0) {
|
|
357
332
|
console.log(`端口 ${port} 没有监听进程。`);
|
|
@@ -362,7 +337,6 @@ async function killPortOnUnix(port) {
|
|
|
362
337
|
console.log(`释放端口 ${port}: 结束 PID ${pid}`);
|
|
363
338
|
await run("kill", ["-9", pid]);
|
|
364
339
|
}
|
|
365
|
-
|
|
366
340
|
console.log(`端口 ${port} 已释放。`);
|
|
367
341
|
}
|
|
368
342
|
|
|
@@ -376,6 +350,26 @@ function addressUsesPort(address, port) {
|
|
|
376
350
|
return address.endsWith(`:${port}`);
|
|
377
351
|
}
|
|
378
352
|
|
|
353
|
+
function run(command, args, options = {}) {
|
|
354
|
+
return new Promise((resolvePromise, reject) => {
|
|
355
|
+
console.log(`> ${command} ${args.join(" ")}`);
|
|
356
|
+
const child = spawn(command, args, {
|
|
357
|
+
cwd: options.cwd ?? process.cwd(),
|
|
358
|
+
stdio: "inherit",
|
|
359
|
+
shell: false
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
child.on("error", reject);
|
|
363
|
+
child.on("exit", (code) => {
|
|
364
|
+
if (code === 0 || options.allowFailure) {
|
|
365
|
+
resolvePromise();
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
reject(new Error(`命令退出码 ${code}: ${command} ${args.join(" ")}`));
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
379
373
|
function capture(command, args, options = {}) {
|
|
380
374
|
return new Promise((resolvePromise, reject) => {
|
|
381
375
|
const child = spawn(command, args, {
|