@wangzhanpeng996/wzp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +119 -0
  3. package/bin/wzp.js +404 -0
  4. package/package.json +29 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # wzp
2
+
3
+ `wzp` 是一个个人命令行工具箱。安装后统一使用 `wzp <命令>` 调用不同功能。
4
+
5
+ 当前已有功能:
6
+
7
+ - `wzp gitee-create`:从一个源仓库克隆项目,删除原 `.git`,自动创建新的 Gitee 仓库,并重新绑定 remote。
8
+ - `wzp create`:`wzp gitee-create` 的兼容别名。
9
+ - `wzp kill`:快速释放本机指定端口,例如 `wzp kill 8080`。
10
+ - `wzp kill-port`:`wzp kill` 的兼容别名。
11
+
12
+ ## 安装
13
+
14
+ 本机开发调试:
15
+
16
+ ```powershell
17
+ npm link
18
+ wzp --help
19
+ ```
20
+
21
+ 发布到 npm 后:
22
+
23
+ ```powershell
24
+ npm i -g @wangzhanpeng996/wzp
25
+ ```
26
+
27
+ ## gitee-create
28
+
29
+ 先在当前 PowerShell 会话设置 Gitee 访问令牌:
30
+
31
+ ```powershell
32
+ $env:GITEE_TOKEN="你的Gitee访问令牌"
33
+ ```
34
+
35
+ 然后在你希望生成项目的目录里执行:
36
+
37
+ ```powershell
38
+ wzp gitee-create https://gitee.com/wzplxt/web1.git my-new-repo
39
+ ```
40
+
41
+ 也可以使用具名参数:
42
+
43
+ ```powershell
44
+ wzp gitee-create `
45
+ --source https://gitee.com/wzplxt/web1.git `
46
+ --repo my-new-repo `
47
+ --description "我的新项目" `
48
+ --private `
49
+ --no-push
50
+ ```
51
+
52
+ 它会做这些事:
53
+
54
+ 1. 检查 `GITEE_TOKEN`,令牌缺失或无效会直接退出。
55
+ 2. 在 Gitee 当前 token 对应的个人账号下创建新仓库。
56
+ 3. 把源仓库克隆到当前运行目录下的 `my-new-repo` 文件夹。
57
+ 4. 删除克隆下来的原 `.git`。
58
+ 5. 重新执行 `git init`。
59
+ 6. 把 `origin` 指向新 Gitee 仓库,默认优先使用 HTTPS 地址。
60
+ 7. 执行 `git add .` 和一次初始化提交。
61
+ 8. 默认继续推送到新 Gitee 仓库;如果加了 `--no-push`,只在本地准备好。
62
+
63
+ 如果加了 `--no-push`,最后会提示完整推送命令,例如:
64
+
65
+ ```powershell
66
+ git -C "F:\0.代码\脚手架维护\test\my-new-repo" push -u origin HEAD
67
+ ```
68
+
69
+ ## 发布
70
+
71
+ ## kill
72
+
73
+ 释放本机端口:
74
+
75
+ ```powershell
76
+ wzp kill 8080
77
+ ```
78
+
79
+ Windows 下会查找正在监听该 TCP 端口的进程,然后执行:
80
+
81
+ ```powershell
82
+ taskkill /PID <进程ID> /F
83
+ ```
84
+
85
+ 如果没有进程监听该端口,会直接提示无需处理。
86
+
87
+ ## 发布
88
+
89
+ 如果这是第一次发布 `wzp` 这个新包名:
90
+
91
+ ```powershell
92
+ npm login
93
+ npm publish --access=public --otp=你的6位验证码
94
+ ```
95
+
96
+ 后续更新:
97
+
98
+ ```powershell
99
+ npm version patch
100
+ npm publish --access=public --otp=你的6位验证码
101
+ ```
102
+
103
+ 别人更新:
104
+
105
+ ```powershell
106
+ npm i -g @wangzhanpeng996/wzp@latest
107
+ ```
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 ADDED
@@ -0,0 +1,404 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from "node:child_process";
4
+ import { existsSync } from "node:fs";
5
+ import { mkdir, readdir, rm, writeFile } from "node:fs/promises";
6
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
7
+ import process from "node:process";
8
+ import readline from "node:readline/promises";
9
+
10
+ const globalHelp = `
11
+ 用法:
12
+ wzp <命令> [参数]
13
+
14
+ 可用命令:
15
+ gitee-create 从源仓库克隆项目,断开原 git 历史,创建并绑定新的 Gitee 仓库
16
+ create gitee-create 的兼容别名
17
+ kill 释放指定端口,例如 wzp kill 8080
18
+ kill-port kill 的兼容别名
19
+
20
+ 示例:
21
+ wzp gitee-create https://gitee.com/wzplxt/web1.git my-new-repo
22
+ wzp gitee-create --source https://gitee.com/wzplxt/web1.git --repo my-new-repo
23
+ wzp kill 8080
24
+
25
+ 查看子命令帮助:
26
+ wzp gitee-create --help
27
+ wzp kill --help
28
+ `;
29
+
30
+ const giteeCreateHelp = `
31
+ 用法:
32
+ wzp gitee-create <源仓库地址> <新仓库名>
33
+ wzp gitee-create --source <源仓库地址> --repo <新仓库名>
34
+
35
+ 常用参数:
36
+ --source <url> 要克隆的原项目地址,也可以作为第 1 个位置参数
37
+ --repo <name> 新 Gitee 仓库路径名,也会作为当前目录下的新项目文件夹名
38
+ --description <text> Gitee 仓库简介
39
+ --private 创建私有 Gitee 仓库
40
+ --no-push 初始化提交后不推送到 origin
41
+ -h, --help 显示帮助
42
+
43
+ 认证方式:
44
+ 启动后会检查环境变量 GITEE_TOKEN。没有令牌或令牌无效会直接退出。
45
+ 不要把 Gitee 密码写进命令或脚本。推送认证交给本机 git 凭据管理器。
46
+ `;
47
+
48
+ const killHelp = `
49
+ 用法:
50
+ wzp kill <端口号>
51
+ wzp kill-port <端口号>
52
+
53
+ 示例:
54
+ wzp kill 8080
55
+
56
+ 说明:
57
+ Windows 下会查找正在监听该 TCP 端口的进程,并执行 taskkill /F。
58
+ 请只对你确认可以关闭的本地开发服务使用。
59
+ `;
60
+
61
+ main().catch((error) => {
62
+ console.error(`\n失败: ${error.message}`);
63
+ process.exit(1);
64
+ });
65
+
66
+ async function main() {
67
+ const args = process.argv.slice(2);
68
+ const command = args[0];
69
+
70
+ if (!command || command === "--help" || command === "-h") {
71
+ console.log(globalHelp.trim());
72
+ return;
73
+ }
74
+
75
+ if (command === "gitee-create" || command === "create") {
76
+ await runGiteeCreate(args.slice(1));
77
+ return;
78
+ }
79
+
80
+ if (command === "kill" || command === "kill-port") {
81
+ await runKillPort(args.slice(1));
82
+ return;
83
+ }
84
+
85
+ throw new Error(`未知命令: ${command}\n\n${globalHelp.trim()}`);
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);
110
+ }
111
+
112
+ async function runGiteeCreate(args) {
113
+ const parsed = parseArgs(args);
114
+ if (parsed.help) {
115
+ console.log(giteeCreateHelp.trim());
116
+ return;
117
+ }
118
+
119
+ const rl = readline.createInterface({
120
+ input: process.stdin,
121
+ output: process.stdout
122
+ });
123
+
124
+ try {
125
+ const source = await requiredValue(rl, parsed, "source", "源仓库地址");
126
+ const repo = await requiredValue(rl, parsed, "repo", "新仓库名");
127
+ const authUser = await validateGiteeToken(process.env.GITEE_TOKEN);
128
+ console.log(`Gitee 令牌有效,当前用户: ${authUser.login || authUser.name || authUser.id}`);
129
+
130
+ const target = resolve(process.cwd(), repo);
131
+ const push = !parsed.options.noPush;
132
+
133
+ await ensureTargetAvailable(target);
134
+
135
+ const remote = await createGiteeRepo({
136
+ owner: authUser.login,
137
+ repo,
138
+ name: repo,
139
+ description: parsed.options.description ?? "",
140
+ isPrivate: Boolean(parsed.options.private),
141
+ token: process.env.GITEE_TOKEN
142
+ });
143
+
144
+ await run("git", ["clone", source, target]);
145
+ await detachGitHistory(target);
146
+ await run("git", ["init"], { cwd: target });
147
+ await run("git", ["remote", "add", "origin", remote], { cwd: target });
148
+ await run("git", ["add", "."], { cwd: target });
149
+ await run("git", ["commit", "-m", "初始化项目"], { cwd: target, allowFailure: true });
150
+
151
+ if (push) {
152
+ await run("git", ["push", "-u", "origin", "HEAD"], { cwd: target });
153
+ }
154
+
155
+ console.log("\n完成");
156
+ console.log(`本地目录: ${target}`);
157
+ console.log(`新 remote: ${remote}`);
158
+ if (!push) {
159
+ console.log(`尚未推送。确认无误后可执行: git -C "${target}" push -u origin HEAD`);
160
+ }
161
+ } finally {
162
+ rl.close();
163
+ }
164
+ }
165
+
166
+ function parseArgs(args) {
167
+ const result = {
168
+ help: args.includes("--help") || args.includes("-h"),
169
+ options: {}
170
+ };
171
+
172
+ const positionals = [];
173
+ for (let i = 0; i < args.length; i += 1) {
174
+ const arg = args[i];
175
+ if (arg === "--help" || arg === "-h") {
176
+ continue;
177
+ }
178
+ if (!arg.startsWith("--")) {
179
+ positionals.push(arg);
180
+ continue;
181
+ }
182
+
183
+ const [rawKey, inlineValue] = arg.slice(2).split("=", 2);
184
+ const key = toCamelCase(rawKey);
185
+ if (["private", "noPush"].includes(key)) {
186
+ result.options[key] = true;
187
+ continue;
188
+ }
189
+
190
+ const value = inlineValue ?? args[i + 1];
191
+ if (!value || value.startsWith("--")) {
192
+ throw new Error(`参数 --${rawKey} 缺少值`);
193
+ }
194
+ result.options[key] = value;
195
+ if (inlineValue === undefined) {
196
+ i += 1;
197
+ }
198
+ }
199
+
200
+ result.options.source ??= positionals[0];
201
+ result.options.repo ??= positionals[1];
202
+ return result;
203
+ }
204
+
205
+ function toCamelCase(value) {
206
+ return value.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
207
+ }
208
+
209
+ async function requiredValue(rl, parsed, key, label) {
210
+ const current = parsed.options[key];
211
+ if (current) {
212
+ return current;
213
+ }
214
+ const answer = await rl.question(`${label}: `);
215
+ if (!answer.trim()) {
216
+ throw new Error(`缺少必填参数: ${label}`);
217
+ }
218
+ return answer.trim();
219
+ }
220
+
221
+ async function ensureTargetAvailable(target) {
222
+ if (!isAbsolute(target)) {
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}`);
234
+ }
235
+ }
236
+
237
+ async function detachGitHistory(target) {
238
+ const gitDir = join(target, ".git");
239
+ const resolvedGitDir = resolve(gitDir);
240
+ const resolvedTarget = resolve(target);
241
+ if (!resolvedGitDir.startsWith(`${resolvedTarget}\\`) && resolvedGitDir !== join(resolvedTarget, ".git")) {
242
+ throw new Error(`拒绝删除异常路径: ${resolvedGitDir}`);
243
+ }
244
+ if (existsSync(gitDir)) {
245
+ await rm(gitDir, { recursive: true, force: true });
246
+ }
247
+ }
248
+
249
+ async function createGiteeRepo({ owner, repo, name, description, isPrivate, token }) {
250
+ const body = new URLSearchParams({
251
+ access_token: token,
252
+ name,
253
+ path: repo,
254
+ description,
255
+ private: String(isPrivate)
256
+ });
257
+
258
+ const response = await fetch("https://gitee.com/api/v5/user/repos", {
259
+ method: "POST",
260
+ headers: {
261
+ "Content-Type": "application/x-www-form-urlencoded"
262
+ },
263
+ body
264
+ });
265
+
266
+ const payload = await response.json().catch(() => ({}));
267
+ if (!response.ok) {
268
+ const message = payload.message || payload.error || response.statusText;
269
+ throw new Error(`Gitee 建仓失败: ${message}`);
270
+ }
271
+
272
+ return payload.html_url || payload.ssh_url || `https://gitee.com/${owner}/${repo}.git`;
273
+ }
274
+
275
+ async function validateGiteeToken(token) {
276
+ if (!token) {
277
+ throw new Error("缺少 GITEE_TOKEN。请先在当前 PowerShell 会话执行: $env:GITEE_TOKEN=\"你的Gitee访问令牌\"");
278
+ }
279
+
280
+ const url = new URL("https://gitee.com/api/v5/user");
281
+ url.searchParams.set("access_token", token);
282
+ const response = await fetch(url);
283
+ const payload = await response.json().catch(() => ({}));
284
+
285
+ if (!response.ok || !payload.id) {
286
+ const message = payload.message || payload.error || response.statusText;
287
+ throw new Error(`GITEE_TOKEN 无效或权限不足: ${message}`);
288
+ }
289
+
290
+ return payload;
291
+ }
292
+
293
+ function run(command, args, options = {}) {
294
+ return new Promise((resolvePromise, reject) => {
295
+ console.log(`> ${command} ${args.join(" ")}`);
296
+ const child = spawn(command, args, {
297
+ cwd: options.cwd ?? process.cwd(),
298
+ stdio: "inherit",
299
+ shell: false
300
+ });
301
+
302
+ child.on("error", reject);
303
+ child.on("exit", (code) => {
304
+ if (code === 0 || options.allowFailure) {
305
+ resolvePromise();
306
+ return;
307
+ }
308
+ reject(new Error(`命令退出码 ${code}: ${command} ${args.join(" ")}`));
309
+ });
310
+ });
311
+ }
312
+
313
+ async function killPortOnWindows(port) {
314
+ const output = await capture("netstat", ["-ano", "-p", "TCP"]);
315
+ const pids = new Set();
316
+
317
+ for (const line of output.split(/\r?\n/)) {
318
+ const trimmed = line.trim();
319
+ if (!trimmed || !trimmed.includes("LISTENING")) {
320
+ continue;
321
+ }
322
+
323
+ const parts = trimmed.split(/\s+/);
324
+ const localAddress = parts[1];
325
+ const state = parts[3];
326
+ const pid = parts[4];
327
+ if (state !== "LISTENING" || !pid) {
328
+ continue;
329
+ }
330
+
331
+ if (addressUsesPort(localAddress, port)) {
332
+ pids.add(pid);
333
+ }
334
+ }
335
+
336
+ if (pids.size === 0) {
337
+ console.log(`端口 ${port} 没有监听进程。`);
338
+ return;
339
+ }
340
+
341
+ for (const pid of pids) {
342
+ console.log(`释放端口 ${port}: 结束 PID ${pid}`);
343
+ await run("taskkill", ["/PID", pid, "/F"]);
344
+ }
345
+
346
+ console.log(`端口 ${port} 已释放。`);
347
+ }
348
+
349
+ async function killPortOnUnix(port) {
350
+ 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);
355
+
356
+ if (pids.length === 0) {
357
+ console.log(`端口 ${port} 没有监听进程。`);
358
+ return;
359
+ }
360
+
361
+ for (const pid of pids) {
362
+ console.log(`释放端口 ${port}: 结束 PID ${pid}`);
363
+ await run("kill", ["-9", pid]);
364
+ }
365
+
366
+ console.log(`端口 ${port} 已释放。`);
367
+ }
368
+
369
+ function addressUsesPort(address, port) {
370
+ if (!address) {
371
+ return false;
372
+ }
373
+ if (address.startsWith("[") && address.includes("]:")) {
374
+ return address.endsWith(`]:${port}`);
375
+ }
376
+ return address.endsWith(`:${port}`);
377
+ }
378
+
379
+ function capture(command, args, options = {}) {
380
+ return new Promise((resolvePromise, reject) => {
381
+ const child = spawn(command, args, {
382
+ cwd: options.cwd ?? process.cwd(),
383
+ stdio: ["ignore", "pipe", "pipe"],
384
+ shell: false
385
+ });
386
+
387
+ let stdout = "";
388
+ let stderr = "";
389
+ child.stdout.on("data", (chunk) => {
390
+ stdout += chunk.toString();
391
+ });
392
+ child.stderr.on("data", (chunk) => {
393
+ stderr += chunk.toString();
394
+ });
395
+ child.on("error", reject);
396
+ child.on("exit", (code) => {
397
+ if (code === 0 || options.allowFailure) {
398
+ resolvePromise(stdout);
399
+ return;
400
+ }
401
+ reject(new Error(`命令退出码 ${code}: ${command} ${args.join(" ")}\n${stderr.trim()}`));
402
+ });
403
+ });
404
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@wangzhanpeng996/wzp",
3
+ "version": "0.1.0",
4
+ "description": "Personal CLI toolbox for Gitee project creation and other WZP workflows.",
5
+ "type": "module",
6
+ "bin": {
7
+ "wzp": "bin/wzp.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "scripts": {
18
+ "start": "node ./bin/wzp.js",
19
+ "check": "node ./bin/wzp.js --help"
20
+ },
21
+ "keywords": [
22
+ "gitee",
23
+ "git",
24
+ "clone",
25
+ "cli",
26
+ "toolbox"
27
+ ],
28
+ "license": "MIT"
29
+ }