@peiyanlu/cli-utils 0.0.4 → 0.0.6
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/dist/index.cjs +1219 -0
- package/dist/index.d.cts +525 -0
- package/dist/index.d.mts +525 -0
- package/dist/index.mjs +1115 -0
- package/package.json +9 -5
- package/dist/cjs/index.cjs +0 -344
- package/dist/cjs/index.d.cts +0 -114
- package/dist/esm/index.d.mts +0 -114
- package/dist/esm/index.mjs +0 -312
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1219 @@
|
|
|
1
|
+
let node_fs = require("node:fs");
|
|
2
|
+
let node_fs_promises = require("node:fs/promises");
|
|
3
|
+
let node_os = require("node:os");
|
|
4
|
+
let node_path = require("node:path");
|
|
5
|
+
let node_child_process = require("node:child_process");
|
|
6
|
+
let node_util = require("node:util");
|
|
7
|
+
|
|
8
|
+
//#region src/enums.ts
|
|
9
|
+
let PkgManager = /* @__PURE__ */ function(PkgManager$1) {
|
|
10
|
+
PkgManager$1["NPM"] = "npm";
|
|
11
|
+
PkgManager$1["YARN"] = "yarn";
|
|
12
|
+
PkgManager$1["PNPM"] = "pnpm";
|
|
13
|
+
return PkgManager$1;
|
|
14
|
+
}({});
|
|
15
|
+
/**
|
|
16
|
+
* @deprecated Use `ConfirmResult` instead.
|
|
17
|
+
*/
|
|
18
|
+
let YesOrNo = /* @__PURE__ */ function(YesOrNo$1) {
|
|
19
|
+
YesOrNo$1["Yes"] = "yes";
|
|
20
|
+
YesOrNo$1["No"] = "no";
|
|
21
|
+
YesOrNo$1["Ignore"] = "ignore";
|
|
22
|
+
return YesOrNo$1;
|
|
23
|
+
}({});
|
|
24
|
+
let ConfirmResult = /* @__PURE__ */ function(ConfirmResult$1) {
|
|
25
|
+
ConfirmResult$1["YES"] = "yes";
|
|
26
|
+
ConfirmResult$1["NO"] = "no";
|
|
27
|
+
ConfirmResult$1["IGNORE"] = "ignore";
|
|
28
|
+
return ConfirmResult$1;
|
|
29
|
+
}({});
|
|
30
|
+
let HttpLibrary = /* @__PURE__ */ function(HttpLibrary$1) {
|
|
31
|
+
HttpLibrary$1["EXPRESS"] = "express";
|
|
32
|
+
HttpLibrary$1["FASTIFY"] = "fastify";
|
|
33
|
+
HttpLibrary$1["KOA"] = "koa";
|
|
34
|
+
HttpLibrary$1["HONO"] = "hono";
|
|
35
|
+
return HttpLibrary$1;
|
|
36
|
+
}({});
|
|
37
|
+
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/styleText.ts
|
|
40
|
+
const dim = (text) => (0, node_util.styleText)(["dim"], text);
|
|
41
|
+
const red = (text) => (0, node_util.styleText)(["red"], text);
|
|
42
|
+
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/shell.ts
|
|
45
|
+
/** 异步执行 `spawn` 获取字符串类型的结果 */
|
|
46
|
+
const spawnAsync = (cmd, args, options) => {
|
|
47
|
+
return new Promise((resolve$2, reject) => {
|
|
48
|
+
const { trim, error, dryRun, ...others } = options ?? {};
|
|
49
|
+
const fullCmd = stringifyArgs([cmd, ...args]);
|
|
50
|
+
if (dryRun) {
|
|
51
|
+
console.log(`${dim("[dry-run]")} ${fullCmd}`);
|
|
52
|
+
return resolve$2(void 0);
|
|
53
|
+
}
|
|
54
|
+
const child = (0, node_child_process.spawn)(cmd, args, { ...others });
|
|
55
|
+
let stdout = "";
|
|
56
|
+
child.stdout?.setEncoding("utf-8");
|
|
57
|
+
child.stdout?.on("data", (d) => {
|
|
58
|
+
stdout += trim ? d.trim() : d;
|
|
59
|
+
});
|
|
60
|
+
let stderr = "";
|
|
61
|
+
child.stderr?.setEncoding("utf-8");
|
|
62
|
+
child.stderr?.on("data", (d) => {
|
|
63
|
+
stderr += trim ? d.trim() : d;
|
|
64
|
+
});
|
|
65
|
+
child.on("error", reject);
|
|
66
|
+
child.on("close", (code) => {
|
|
67
|
+
if (code !== 0) {
|
|
68
|
+
const msg = `${red("spawnAsync")} ${dim(fullCmd)} ${stderr}`;
|
|
69
|
+
if (error === "log") {
|
|
70
|
+
console.error(msg);
|
|
71
|
+
return resolve$2(void 0);
|
|
72
|
+
}
|
|
73
|
+
if (error === "throw") return reject(new Error(msg));
|
|
74
|
+
return resolve$2(void 0);
|
|
75
|
+
}
|
|
76
|
+
resolve$2(stdout);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
/** 异步执行 `exec` 获取字符串类型的结果 */
|
|
81
|
+
const execAsync = (cmd, argsOrOptions, maybeOptions) => {
|
|
82
|
+
return new Promise((resolve$2, reject) => {
|
|
83
|
+
let command;
|
|
84
|
+
let options;
|
|
85
|
+
if (Array.isArray(argsOrOptions)) {
|
|
86
|
+
command = stringifyArgs([cmd, ...argsOrOptions]);
|
|
87
|
+
options = maybeOptions;
|
|
88
|
+
} else {
|
|
89
|
+
command = cmd;
|
|
90
|
+
options = argsOrOptions;
|
|
91
|
+
}
|
|
92
|
+
const { trim, dryRun, error, ...others } = options ?? {};
|
|
93
|
+
if (dryRun) {
|
|
94
|
+
console.log(`${dim("[dry-run]")} ${command}`);
|
|
95
|
+
return resolve$2(void 0);
|
|
96
|
+
}
|
|
97
|
+
(0, node_child_process.exec)(command, { ...others }, (err, stdout, stderr) => {
|
|
98
|
+
if (err) {
|
|
99
|
+
const msg = `${red("execAsync")} ${dim(command)} ${stderr || err.message}`;
|
|
100
|
+
if (error === "log") {
|
|
101
|
+
console.error(msg);
|
|
102
|
+
return resolve$2(void 0);
|
|
103
|
+
}
|
|
104
|
+
if (error === "throw") return reject(new Error(msg));
|
|
105
|
+
return resolve$2(void 0);
|
|
106
|
+
}
|
|
107
|
+
resolve$2(trim ? stdout.trim() : stdout);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
/** 执行 `spawnSync` 获取字符串类型的结果 */
|
|
112
|
+
const spawnSyncWithString = (cmd, args, options) => {
|
|
113
|
+
const { trim, error, dryRun, ...others } = options ?? {};
|
|
114
|
+
const fullCmd = stringifyArgs([cmd, ...args]);
|
|
115
|
+
if (dryRun) {
|
|
116
|
+
console.log(`${dim("[dry-run]")} ${fullCmd}`);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const { stdout, stderr, status, error: err } = (0, node_child_process.spawnSync)(cmd, args, {
|
|
120
|
+
encoding: "utf-8",
|
|
121
|
+
...others
|
|
122
|
+
});
|
|
123
|
+
if (status !== 0 || err) {
|
|
124
|
+
const detail = err?.message || stderr?.toString?.() || "";
|
|
125
|
+
const msg = `${red("spawnSync")} ${dim(fullCmd)} ${detail}`;
|
|
126
|
+
if (error === "log") {
|
|
127
|
+
console.error(msg);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (error === "throw") throw new Error(msg);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
return trim ? stdout.trim() : stdout;
|
|
134
|
+
};
|
|
135
|
+
/** 执行 `execSync` 获取字符串类型的结果 */
|
|
136
|
+
const execSyncWithString = (cmd, argsOrOptions, maybeOptions) => {
|
|
137
|
+
let command;
|
|
138
|
+
let options;
|
|
139
|
+
if (Array.isArray(argsOrOptions)) {
|
|
140
|
+
command = stringifyArgs([cmd, ...argsOrOptions]);
|
|
141
|
+
options = maybeOptions;
|
|
142
|
+
} else {
|
|
143
|
+
command = cmd;
|
|
144
|
+
options = argsOrOptions;
|
|
145
|
+
}
|
|
146
|
+
const { trim, dryRun, error, ...others } = options ?? {};
|
|
147
|
+
if (dryRun) {
|
|
148
|
+
console.log(`${dim("[dry-run]")} ${command}`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
const stdout = (0, node_child_process.execSync)(command, {
|
|
153
|
+
encoding: "utf-8",
|
|
154
|
+
...others
|
|
155
|
+
});
|
|
156
|
+
return trim ? stdout.trim() : stdout;
|
|
157
|
+
} catch (e) {
|
|
158
|
+
const stderr = e?.stderr?.toString?.() || e?.message || "";
|
|
159
|
+
const msg = `${red("execSync")} ${dim(command)} ${stderr}`;
|
|
160
|
+
if (error === "log") {
|
|
161
|
+
console.error(msg);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (error === "throw") throw new Error(msg);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
/** 基于 {@link spawnAsync} 实现 */
|
|
169
|
+
const runGit = async (args, options = { trim: true }) => {
|
|
170
|
+
return spawnAsync("git", args, options);
|
|
171
|
+
};
|
|
172
|
+
/** 基于 {@link spawnSyncWithString} 实现 */
|
|
173
|
+
const runGitSync = (args, options) => {
|
|
174
|
+
return spawnSyncWithString("git", args, { ...options });
|
|
175
|
+
};
|
|
176
|
+
/** 基于 {@link execAsync} 实现 */
|
|
177
|
+
const runNpm = (args, options = { trim: true }) => {
|
|
178
|
+
return execAsync("npm", args, options);
|
|
179
|
+
};
|
|
180
|
+
/** 基于 {@link execSyncWithString} 实现 */
|
|
181
|
+
const runNpmSync = (args, options) => {
|
|
182
|
+
return execSyncWithString("npm", args, { ...options });
|
|
183
|
+
};
|
|
184
|
+
/** 基于 {@link spawnAsync} 实现 */
|
|
185
|
+
const runNode = (args, options) => {
|
|
186
|
+
return spawnAsync("node", args, options);
|
|
187
|
+
};
|
|
188
|
+
/** 基于 {@link spawnSyncWithString} 实现 */
|
|
189
|
+
const runNodeSync = (args, options) => {
|
|
190
|
+
return spawnSyncWithString("node", args, options);
|
|
191
|
+
};
|
|
192
|
+
/** 支持所有支持 `--version` 命令的脚本查看版本 */
|
|
193
|
+
const checkVersion = async (cmd) => {
|
|
194
|
+
return execAsync(`${cmd} --version`);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
//#endregion
|
|
198
|
+
//#region src/utils.ts
|
|
199
|
+
const isValidPackageName = (packageName) => {
|
|
200
|
+
return /^(?:@[a-z\d\-*~][a-z\d\-*._~]*\/)?[a-z\d\-~][a-z\d\-._~]*$/.test(packageName);
|
|
201
|
+
};
|
|
202
|
+
const toValidPackageName = (packageName) => packageName.trim().toLowerCase().replace(/\s+/g, "-").replace(/^[._]/, "").replace(/[^a-z\d\-~]+/g, "-");
|
|
203
|
+
const toValidProjectName = (projectName) => projectName.trim().replace(/\/+$/g, "");
|
|
204
|
+
const emptyDir = async (dir, ignore = []) => {
|
|
205
|
+
if (!(0, node_fs.existsSync)(dir)) return false;
|
|
206
|
+
for (const file of await (0, node_fs_promises.readdir)(dir)) {
|
|
207
|
+
if (ignore.includes(file)) continue;
|
|
208
|
+
await (0, node_fs_promises.rm)((0, node_path.resolve)(dir, file), {
|
|
209
|
+
recursive: true,
|
|
210
|
+
force: true
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return true;
|
|
214
|
+
};
|
|
215
|
+
const isEmpty = async (path, ignore = []) => {
|
|
216
|
+
return (await (0, node_fs_promises.readdir)(path)).filter((f) => !ignore.includes(f)).length === 0;
|
|
217
|
+
};
|
|
218
|
+
const editFile = async (file, callback) => {
|
|
219
|
+
if (!(0, node_fs.existsSync)(file)) return;
|
|
220
|
+
return (0, node_fs_promises.writeFile)(file, callback(await (0, node_fs_promises.readFile)(file, "utf-8")), "utf-8");
|
|
221
|
+
};
|
|
222
|
+
const editJsonFile = (file, callback) => {
|
|
223
|
+
return editFile(file, (str) => {
|
|
224
|
+
try {
|
|
225
|
+
const json = JSON.parse(str);
|
|
226
|
+
callback(json);
|
|
227
|
+
return JSON.stringify(json, null, 2);
|
|
228
|
+
} catch (e) {
|
|
229
|
+
console.error(e);
|
|
230
|
+
return str;
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
};
|
|
234
|
+
const readSubDirs = async (source, ignore = []) => {
|
|
235
|
+
return (await (0, node_fs_promises.readdir)(source, { withFileTypes: true })).filter((k) => k.isDirectory() && !ignore.includes(k.name)).map((dir) => dir.name);
|
|
236
|
+
};
|
|
237
|
+
const copyDirAsync = async (src, dest, options) => {
|
|
238
|
+
await (0, node_fs_promises.mkdir)(dest, { recursive: true });
|
|
239
|
+
const entries = await (0, node_fs_promises.readdir)(src, { withFileTypes: true });
|
|
240
|
+
for (const entry of entries) {
|
|
241
|
+
const name = entry.name;
|
|
242
|
+
const isDir = entry.isDirectory();
|
|
243
|
+
const { rename = {}, skips = [] } = options;
|
|
244
|
+
const relName = rename[name] ?? name;
|
|
245
|
+
if (skips.some((rule) => rule(name, isDir))) continue;
|
|
246
|
+
const from = (0, node_path.join)(src, name);
|
|
247
|
+
const to = (0, node_path.join)(dest, relName);
|
|
248
|
+
if (isDir) await copyDirAsync(from, to, options);
|
|
249
|
+
else await (0, node_fs_promises.copyFile)(from, to);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
const readJsonFile = (file) => {
|
|
253
|
+
if (!(0, node_fs.existsSync)(file)) return {};
|
|
254
|
+
try {
|
|
255
|
+
return JSON.parse((0, node_fs.readFileSync)(file, "utf-8"));
|
|
256
|
+
} catch (e) {
|
|
257
|
+
return {};
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
const getPackageInfo = (pkgName, getPkgDir) => {
|
|
261
|
+
const pkgDir = (0, node_path.resolve)(getPkgDir(pkgName));
|
|
262
|
+
const pkgPath = (0, node_path.resolve)(pkgDir, "package.json");
|
|
263
|
+
return {
|
|
264
|
+
pkg: readJsonFile(pkgPath),
|
|
265
|
+
pkgDir,
|
|
266
|
+
pkgPath
|
|
267
|
+
};
|
|
268
|
+
};
|
|
269
|
+
/** 通过包管理器执行脚本时生效 UserAgent: `process.env.npm_config_user_agent` */
|
|
270
|
+
const pkgFromUserAgent = (userAgent) => {
|
|
271
|
+
if (!userAgent) return void 0;
|
|
272
|
+
const [name, version] = userAgent.split(" ")[0].split("/");
|
|
273
|
+
return {
|
|
274
|
+
name,
|
|
275
|
+
version
|
|
276
|
+
};
|
|
277
|
+
};
|
|
278
|
+
/** 同步执行 Node CLI(用于测试环境) */
|
|
279
|
+
const runCliForTest = (path, args = [], options) => {
|
|
280
|
+
return runNodeSync([path, ...args], {
|
|
281
|
+
env: {
|
|
282
|
+
...process.env,
|
|
283
|
+
_VITE_TEST_CLI: "true"
|
|
284
|
+
},
|
|
285
|
+
...options
|
|
286
|
+
});
|
|
287
|
+
};
|
|
288
|
+
/** 判断测试文件(夹) */
|
|
289
|
+
const isTestFile = (name) => {
|
|
290
|
+
return [
|
|
291
|
+
/(^|[\\/])(test(s?)|__test(s?)__)([\\/]|$)/,
|
|
292
|
+
/\.([a-zA-Z0-9]+-)?(test|spec)\.m?(ts|js)$/,
|
|
293
|
+
/^vitest([-.])(.*)\.m?(ts|js)$/
|
|
294
|
+
].some((reg) => reg.test(name));
|
|
295
|
+
};
|
|
296
|
+
/** 解析 Github 链接获取 owner 和 repo */
|
|
297
|
+
const parseGitHubRepo = (url) => {
|
|
298
|
+
const match = url.trim().match(/github(?:\.com)?[:/](.+?)\/(.+?)(?:[#/?].+?)?(?:\.git)?$/);
|
|
299
|
+
return match ? match.slice(1, 3) : [];
|
|
300
|
+
};
|
|
301
|
+
/** 基于 EOL 的可多换行函数 */
|
|
302
|
+
const eol = (n = 1) => node_os.EOL.repeat(n);
|
|
303
|
+
/** 多空格函数 */
|
|
304
|
+
const space = (n = 1) => " ".repeat(n);
|
|
305
|
+
/** 将字符串以空格分割为数组 */
|
|
306
|
+
const parseArgs = (args) => args.trim() ? args.trim().split(space()) : [];
|
|
307
|
+
/** 将数组以空格拼接为字符串 */
|
|
308
|
+
const stringifyArgs = (args) => args.length ? args.join(space()) : "";
|
|
309
|
+
/** 去掉模板字符串首尾换行 */
|
|
310
|
+
const trimTemplate = (str) => str.replace(/^\s*\n+|\n+\s*$/g, "");
|
|
311
|
+
/** 生成 GitHub 仓库主页地址 */
|
|
312
|
+
const getGithubUrl = (owner, repo) => {
|
|
313
|
+
return `https://github.com/${owner}/${repo}`;
|
|
314
|
+
};
|
|
315
|
+
/** 生成 GitHub Release 页面地址 */
|
|
316
|
+
const getGithubReleaseUrl = (owner, repo, tag) => {
|
|
317
|
+
return `${getGithubUrl(owner, repo)}/releases/tag/${encodeURIComponent(tag)}`;
|
|
318
|
+
};
|
|
319
|
+
/** 生成 npm 包指定版本的详情页地址 */
|
|
320
|
+
const getPackageUrl = (pkg, version) => {
|
|
321
|
+
return `https://www.npmjs.com/package/${pkg}/v/${version}`;
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
//#endregion
|
|
325
|
+
//#region src/joinUrl.ts
|
|
326
|
+
function joinUrl(input) {
|
|
327
|
+
const temps = Array.isArray(input) ? input : [...arguments];
|
|
328
|
+
if (temps.length === 0) return "";
|
|
329
|
+
const result = [];
|
|
330
|
+
const parts = [...temps];
|
|
331
|
+
/** 协议正则 */
|
|
332
|
+
const PROTOCOL_RE = /^[^/:]+:\/*$/;
|
|
333
|
+
const FILE_PROTOCOL_RE = /^file:\/\/\//;
|
|
334
|
+
if (PROTOCOL_RE.test(parts[0]) && parts.length > 1) {
|
|
335
|
+
parts[1] = parts[0] + parts[1];
|
|
336
|
+
parts.shift();
|
|
337
|
+
}
|
|
338
|
+
if (FILE_PROTOCOL_RE.test(parts[0])) parts[0] = parts[0].replace(/^([^/:]+):\/*/, "$1:///");
|
|
339
|
+
else parts[0] = parts[0].replace(/^([^/:]+):\/*/, "$1://");
|
|
340
|
+
parts.forEach((part, index) => {
|
|
341
|
+
if (!part) return;
|
|
342
|
+
let segment = part;
|
|
343
|
+
if (index > 0) segment = segment.replace(/^\/+/, "");
|
|
344
|
+
if (index < parts.length - 1) segment = segment.replace(/\/+$/, "");
|
|
345
|
+
else segment = segment.replace(/\/+$/, "/");
|
|
346
|
+
result.push(segment);
|
|
347
|
+
});
|
|
348
|
+
let url = result.join("/");
|
|
349
|
+
url = url.replace(/\/(\?|&|#[^!])/g, "$1");
|
|
350
|
+
const [base, ...queryParts] = url.split("?");
|
|
351
|
+
url = base + (queryParts.length ? "?" + queryParts.join("&") : "");
|
|
352
|
+
return url;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
//#endregion
|
|
356
|
+
//#region src/git.ts
|
|
357
|
+
/** 判断指定目录是否是 git 仓库 */
|
|
358
|
+
const isGitRepo = async (dir = ".") => {
|
|
359
|
+
const target = (0, node_path.resolve)(process.cwd(), dir);
|
|
360
|
+
return "true" === await runGit([
|
|
361
|
+
"-C",
|
|
362
|
+
target,
|
|
363
|
+
"rev-parse",
|
|
364
|
+
"--is-inside-work-tree"
|
|
365
|
+
]).catch((err) => {
|
|
366
|
+
if (err?.message?.includes("safe.directory")) console.warn(`⚠️ Git safe.directory restrictions: Please run:\ngit config --global --add safe.directory ${target}`);
|
|
367
|
+
return false;
|
|
368
|
+
});
|
|
369
|
+
};
|
|
370
|
+
/** 获取指定的 git 配置 */
|
|
371
|
+
const getGitConfig = (key, global = true) => {
|
|
372
|
+
return runGit([
|
|
373
|
+
"config",
|
|
374
|
+
...global ? ["--global"] : [],
|
|
375
|
+
key
|
|
376
|
+
]);
|
|
377
|
+
};
|
|
378
|
+
/** 获取 git 远程地址 */
|
|
379
|
+
const getGitRemoteUrl = async (remoteName = "origin") => {
|
|
380
|
+
return runGit([
|
|
381
|
+
"remote",
|
|
382
|
+
"get-url",
|
|
383
|
+
remoteName
|
|
384
|
+
]).catch((_) => runGit([
|
|
385
|
+
"config",
|
|
386
|
+
"--get",
|
|
387
|
+
`remote.${remoteName}.url`
|
|
388
|
+
]));
|
|
389
|
+
};
|
|
390
|
+
/** 获取当前分支 */
|
|
391
|
+
const getCurrentBranch = async () => {
|
|
392
|
+
let branch = await runGit(["branch", "--show-current"]);
|
|
393
|
+
if (!branch) {
|
|
394
|
+
branch = await runGit([
|
|
395
|
+
"rev-parse",
|
|
396
|
+
"--abbrev-ref",
|
|
397
|
+
"HEAD"
|
|
398
|
+
]);
|
|
399
|
+
if (branch === "HEAD") return void 0;
|
|
400
|
+
}
|
|
401
|
+
return branch;
|
|
402
|
+
};
|
|
403
|
+
/** 获取分支关联的远程地址 */
|
|
404
|
+
const getRemoteForBranch = (branch) => {
|
|
405
|
+
return runGit([
|
|
406
|
+
"config",
|
|
407
|
+
"--get",
|
|
408
|
+
`branch.${branch}.remote`
|
|
409
|
+
]);
|
|
410
|
+
};
|
|
411
|
+
/** 获取关联的所有远程地址 */
|
|
412
|
+
const getAllRemotes = async () => {
|
|
413
|
+
return (await runGit(["remote"]))?.split("\n").filter(Boolean) ?? [];
|
|
414
|
+
};
|
|
415
|
+
/** 获取默认的远程地址 */
|
|
416
|
+
const getDefaultRemote = async (branch) => {
|
|
417
|
+
const targetBranch = branch || await getCurrentBranch();
|
|
418
|
+
return targetBranch ? await getRemoteForBranch(targetBranch) : void 0;
|
|
419
|
+
};
|
|
420
|
+
/** 获取默认远程地址之外的远程地址 */
|
|
421
|
+
const getOtherRemotes = async (branch) => {
|
|
422
|
+
const defaultRemote = await getDefaultRemote(branch);
|
|
423
|
+
return (await getAllRemotes()).filter((r) => r !== defaultRemote);
|
|
424
|
+
};
|
|
425
|
+
/** 提取所有分支 */
|
|
426
|
+
const fetchAllBranch = (remoteName = "origin") => {
|
|
427
|
+
return runGit([
|
|
428
|
+
"fetch",
|
|
429
|
+
remoteName,
|
|
430
|
+
"--recurse-submodules=no",
|
|
431
|
+
"--prune"
|
|
432
|
+
]);
|
|
433
|
+
};
|
|
434
|
+
/**
|
|
435
|
+
* 获取本地所有 tag
|
|
436
|
+
* @returns {Promise<string[]>}
|
|
437
|
+
* @defaults git tag --list
|
|
438
|
+
*/
|
|
439
|
+
const getLocalTags = async () => {
|
|
440
|
+
const tags = await runGit(["tag", "--list"]);
|
|
441
|
+
return tags ? tags.split("\n").filter(Boolean) : [];
|
|
442
|
+
};
|
|
443
|
+
/** 获取远程 tags */
|
|
444
|
+
const getSortedTags = async (match = "*", exclude = "*-beta.*", sort = "v:refname", count = 0) => {
|
|
445
|
+
const res = await runGit([
|
|
446
|
+
"for-each-ref",
|
|
447
|
+
"--format=%(refname:short)",
|
|
448
|
+
`--sort=-${sort}`,
|
|
449
|
+
`--exclude=${exclude}`,
|
|
450
|
+
`--count=${count}`,
|
|
451
|
+
`refs/tags/${match}`
|
|
452
|
+
]);
|
|
453
|
+
return res ? res.split("\n").filter(Boolean) : [];
|
|
454
|
+
};
|
|
455
|
+
/**
|
|
456
|
+
* 获取远程(或所有 refs)中的 tag
|
|
457
|
+
* 使用 for-each-ref 以支持版本号排序
|
|
458
|
+
* @param {string} match 默认 *
|
|
459
|
+
* @param {string} exclude 默认 beta
|
|
460
|
+
* @returns {Promise<string[]>}
|
|
461
|
+
* @defaults git for-each-ref --sort=-v:refname --format=%(refname:short) refs/tags/<match>
|
|
462
|
+
*/
|
|
463
|
+
const getRemoteTags = async (match = "*", exclude = "*-beta.*") => {
|
|
464
|
+
return getSortedTags(match, exclude);
|
|
465
|
+
};
|
|
466
|
+
/**
|
|
467
|
+
* 获取当前工作区状态(不包含未跟踪文件)
|
|
468
|
+
* @returns {Promise<string | undefined>}
|
|
469
|
+
* @defaults git status --short --untracked-files=no
|
|
470
|
+
*/
|
|
471
|
+
const getStatus = () => {
|
|
472
|
+
return runGit([
|
|
473
|
+
"status",
|
|
474
|
+
"--short",
|
|
475
|
+
"--untracked-files=no"
|
|
476
|
+
], { trim: false });
|
|
477
|
+
};
|
|
478
|
+
/**
|
|
479
|
+
* 为 git status / changeset 输出添加颜色
|
|
480
|
+
*
|
|
481
|
+
* M -> 黄色(修改)
|
|
482
|
+
* A -> 绿色(新增)
|
|
483
|
+
* D -> 红色(删除)
|
|
484
|
+
* @param {string} log git status --short 输出
|
|
485
|
+
* @returns {string}
|
|
486
|
+
*/
|
|
487
|
+
const coloredChangeset = (log) => {
|
|
488
|
+
const colorStatusChar = (ch) => {
|
|
489
|
+
switch (ch) {
|
|
490
|
+
case "M": return `\x1b[33m${ch}\x1b[0m\x1b[2m`;
|
|
491
|
+
case "A": return `\x1b[32m${ch}\x1b[0m\x1b[2m`;
|
|
492
|
+
case "D": return `\x1b[31m${ch}\x1b[0m\x1b[2m`;
|
|
493
|
+
default: return ch;
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
const colorStatus = (status) => status.split("").map(colorStatusChar).join("");
|
|
497
|
+
return log.split("\n").filter(Boolean).map((line) => {
|
|
498
|
+
const status = line.slice(0, 2);
|
|
499
|
+
const file = line.slice(3);
|
|
500
|
+
return `${colorStatus(status)} ${file}`;
|
|
501
|
+
}).join("\n");
|
|
502
|
+
};
|
|
503
|
+
/**
|
|
504
|
+
* 获取当前分支对应的远程信息
|
|
505
|
+
* @returns {Promise<{remoteName: string, remoteUrl: string | undefined}>}
|
|
506
|
+
*/
|
|
507
|
+
const getRemote = async () => {
|
|
508
|
+
const branch = await getCurrentBranch();
|
|
509
|
+
const remoteName = branch && await getRemoteForBranch(branch) || "origin";
|
|
510
|
+
return {
|
|
511
|
+
remoteName,
|
|
512
|
+
remoteUrl: await runGit([
|
|
513
|
+
"remote",
|
|
514
|
+
"get-url",
|
|
515
|
+
remoteName
|
|
516
|
+
]).catch((_) => runGit([
|
|
517
|
+
"config",
|
|
518
|
+
"--get",
|
|
519
|
+
`remote.${remoteName}.url`
|
|
520
|
+
]))
|
|
521
|
+
};
|
|
522
|
+
};
|
|
523
|
+
/**
|
|
524
|
+
* 获取最新 tag
|
|
525
|
+
*
|
|
526
|
+
* 默认:
|
|
527
|
+
* - 匹配所有 tag
|
|
528
|
+
* - 排除 beta 版本
|
|
529
|
+
* @param {string} match 默认 *
|
|
530
|
+
* @param {string} exclude 默认 beta
|
|
531
|
+
* @returns {Promise<string | undefined>}
|
|
532
|
+
*/
|
|
533
|
+
const getLatestTag = async (match = "*", exclude = "*-beta.*") => {
|
|
534
|
+
const [latestTag] = await getSortedTags(match, exclude, void 0, 1);
|
|
535
|
+
return latestTag;
|
|
536
|
+
};
|
|
537
|
+
/**
|
|
538
|
+
* 获取上一个 tag
|
|
539
|
+
*
|
|
540
|
+
* 默认:
|
|
541
|
+
* - 匹配所有 tag
|
|
542
|
+
* - 排除 beta 版本
|
|
543
|
+
* @param {string} latestTag
|
|
544
|
+
* @param {string} match 默认 *
|
|
545
|
+
* @param {string} exclude 默认 beta
|
|
546
|
+
* @returns {Promise<string | undefined>}
|
|
547
|
+
*/
|
|
548
|
+
const getPreviousTag = async (latestTag, match = "*", exclude = "*-beta.*") => {
|
|
549
|
+
const all = await getSortedTags(match, exclude, void 0, 2);
|
|
550
|
+
return all[all.findIndex((k) => latestTag === k) + 1];
|
|
551
|
+
};
|
|
552
|
+
/**
|
|
553
|
+
* 计算 changelog 的 commit 范围
|
|
554
|
+
* @param {boolean} isIncrement 是否为版本递增发布
|
|
555
|
+
* @param {string} match tag match
|
|
556
|
+
* @param {string} exclude tag exclude
|
|
557
|
+
* @returns {Promise<{from: string, to: string}>}
|
|
558
|
+
*/
|
|
559
|
+
const resolveChangelogRange = async (isIncrement = true, match = "*", exclude = "*-beta.*") => {
|
|
560
|
+
const latestTag = await getLatestTag(match, exclude);
|
|
561
|
+
if (!latestTag) return {
|
|
562
|
+
from: "",
|
|
563
|
+
to: "HEAD"
|
|
564
|
+
};
|
|
565
|
+
const previousTag = await getPreviousTag(latestTag);
|
|
566
|
+
if (!isIncrement && previousTag) return {
|
|
567
|
+
from: previousTag,
|
|
568
|
+
to: `${latestTag}^1`
|
|
569
|
+
};
|
|
570
|
+
return {
|
|
571
|
+
from: latestTag,
|
|
572
|
+
to: "HEAD"
|
|
573
|
+
};
|
|
574
|
+
};
|
|
575
|
+
/**
|
|
576
|
+
* 将短 hash 解析为完整 hash
|
|
577
|
+
* @param {string} short
|
|
578
|
+
* @returns {Promise<string | undefined>}
|
|
579
|
+
*/
|
|
580
|
+
const getFullHash = (short) => {
|
|
581
|
+
return runGit(["rev-parse", short]);
|
|
582
|
+
};
|
|
583
|
+
/**
|
|
584
|
+
* 获取指定范围内的 commit 日志
|
|
585
|
+
* @param {string} from
|
|
586
|
+
* @param {string} to
|
|
587
|
+
* @param {string} scope 文件或目录范围
|
|
588
|
+
* @returns {Promise<string | undefined>}
|
|
589
|
+
*/
|
|
590
|
+
const getLog = async (from = "", to = "HEAD", scope) => {
|
|
591
|
+
const cmd = ["log", `--pretty=format:* %s (%h)`];
|
|
592
|
+
if (from) cmd.push(`${from}...${to}`);
|
|
593
|
+
if (scope) cmd.push(...["--", scope]);
|
|
594
|
+
return runGit(cmd, { trim: false });
|
|
595
|
+
};
|
|
596
|
+
/**
|
|
597
|
+
* 判断工作区是否干净(无未提交修改)
|
|
598
|
+
* @returns {Promise<boolean>}
|
|
599
|
+
*/
|
|
600
|
+
const isWorkingDirClean = async () => {
|
|
601
|
+
return (await runGit(["status", "-s"]))?.length === 0;
|
|
602
|
+
};
|
|
603
|
+
/** 判断字符串是否为合法 remote 名称 */
|
|
604
|
+
const isRemoteName = (remoteName) => {
|
|
605
|
+
return remoteName && !remoteName.includes("/");
|
|
606
|
+
};
|
|
607
|
+
/** 判断当前分支是否已设置 upstream */
|
|
608
|
+
const hasUpstreamBranch = async () => {
|
|
609
|
+
const branch = await runGit([
|
|
610
|
+
"for-each-ref",
|
|
611
|
+
"--format=%(upstream:short)",
|
|
612
|
+
await runGit(["symbolic-ref", "HEAD"])
|
|
613
|
+
]);
|
|
614
|
+
return Boolean(branch);
|
|
615
|
+
};
|
|
616
|
+
/** 获取 push 所需的 upstream 参数 */
|
|
617
|
+
const getUpstreamArgs = async (remoteName, branch) => {
|
|
618
|
+
const hasUpstream = await hasUpstreamBranch();
|
|
619
|
+
const target = branch || await getCurrentBranch();
|
|
620
|
+
if (!hasUpstream) return [
|
|
621
|
+
"--set-upstream",
|
|
622
|
+
remoteName,
|
|
623
|
+
target
|
|
624
|
+
];
|
|
625
|
+
return [remoteName, target];
|
|
626
|
+
};
|
|
627
|
+
/** 统计自最新 tag 以来的提交数量 */
|
|
628
|
+
const countCommitsSinceLatestTag = async () => {
|
|
629
|
+
const latestTag = await getLatestTag();
|
|
630
|
+
return runGit([
|
|
631
|
+
"rev-list",
|
|
632
|
+
latestTag ? `${latestTag}...HEAD` : "HEAD",
|
|
633
|
+
"--count"
|
|
634
|
+
]).then(Number);
|
|
635
|
+
};
|
|
636
|
+
/** git add(包含未追踪文件) */
|
|
637
|
+
const gitAddAll = async (args = []) => {
|
|
638
|
+
await runGit([
|
|
639
|
+
"add",
|
|
640
|
+
"--all",
|
|
641
|
+
...args
|
|
642
|
+
]);
|
|
643
|
+
};
|
|
644
|
+
/** git add(仅已追踪文件) */
|
|
645
|
+
const gitAddTracked = async (args = []) => {
|
|
646
|
+
await runGit([
|
|
647
|
+
"add",
|
|
648
|
+
"--update",
|
|
649
|
+
...args
|
|
650
|
+
]);
|
|
651
|
+
};
|
|
652
|
+
/** git commit */
|
|
653
|
+
const gitCommit = async (message, args = []) => {
|
|
654
|
+
await runGit([
|
|
655
|
+
"commit",
|
|
656
|
+
"--message",
|
|
657
|
+
message,
|
|
658
|
+
...args
|
|
659
|
+
]);
|
|
660
|
+
};
|
|
661
|
+
/** git commit amend */
|
|
662
|
+
const gitCommitAmend = async (message, args = []) => {
|
|
663
|
+
await runGit([
|
|
664
|
+
"commit",
|
|
665
|
+
"--amend",
|
|
666
|
+
"--message",
|
|
667
|
+
message,
|
|
668
|
+
...args
|
|
669
|
+
]);
|
|
670
|
+
};
|
|
671
|
+
/** 创建 annotated tag */
|
|
672
|
+
const gitTagAnnotated = async (tag, message = tag, args = []) => {
|
|
673
|
+
await runGit([
|
|
674
|
+
"tag",
|
|
675
|
+
"--annotate",
|
|
676
|
+
"--message",
|
|
677
|
+
message,
|
|
678
|
+
tag,
|
|
679
|
+
...args
|
|
680
|
+
]);
|
|
681
|
+
};
|
|
682
|
+
/** 创建 lightweight tag */
|
|
683
|
+
const gitTagLightweight = async (tag, message = tag, args = []) => {
|
|
684
|
+
await runGit([
|
|
685
|
+
"tag",
|
|
686
|
+
"--message",
|
|
687
|
+
message,
|
|
688
|
+
tag,
|
|
689
|
+
...args
|
|
690
|
+
]);
|
|
691
|
+
};
|
|
692
|
+
/**
|
|
693
|
+
* 推送 tag 到远程
|
|
694
|
+
* @param {string} remoteName
|
|
695
|
+
* @param {string} tag
|
|
696
|
+
* @param {string[]} args
|
|
697
|
+
* @returns {Promise<string | undefined>}
|
|
698
|
+
* @defaults git push <remoteName> refs/tags/<tag>
|
|
699
|
+
*/
|
|
700
|
+
const pushTag = async (remoteName, tag, args = []) => {
|
|
701
|
+
await runGit([
|
|
702
|
+
"push",
|
|
703
|
+
remoteName,
|
|
704
|
+
`refs/tags/${tag}`,
|
|
705
|
+
...args
|
|
706
|
+
]);
|
|
707
|
+
};
|
|
708
|
+
/**
|
|
709
|
+
* 推送分支到远程
|
|
710
|
+
* 自动处理 upstream 设置
|
|
711
|
+
* @param {string} remoteName
|
|
712
|
+
* @param {string} branch
|
|
713
|
+
* @param {string[]} args
|
|
714
|
+
* @returns {Promise<string | undefined>}
|
|
715
|
+
*/
|
|
716
|
+
const pushBranch = async (remoteName, branch, args = []) => {
|
|
717
|
+
await runGit([
|
|
718
|
+
"push",
|
|
719
|
+
...await getUpstreamArgs(remoteName, branch),
|
|
720
|
+
...args
|
|
721
|
+
]);
|
|
722
|
+
};
|
|
723
|
+
/**
|
|
724
|
+
* 撤销【工作区】中某个文件的修改(未暂存的改动)
|
|
725
|
+
* - 不影响暂存区
|
|
726
|
+
* - 不影响提交历史
|
|
727
|
+
* - ⚠️ 会丢弃该文件当前未暂存的修改
|
|
728
|
+
*
|
|
729
|
+
* 等价命令:git restore <file>
|
|
730
|
+
*/
|
|
731
|
+
const restoreFile = async (file) => {
|
|
732
|
+
await runGit(["restore", file]);
|
|
733
|
+
};
|
|
734
|
+
/**
|
|
735
|
+
* 撤销【工作区】中所有未暂存的修改
|
|
736
|
+
* - 不影响暂存区
|
|
737
|
+
* - 不影响提交历史
|
|
738
|
+
* - ⚠️ 会丢弃所有未暂存的修改
|
|
739
|
+
*
|
|
740
|
+
* 等价命令:git restore .
|
|
741
|
+
*/
|
|
742
|
+
const restoreAll = async () => {
|
|
743
|
+
await runGit(["restore", "."]);
|
|
744
|
+
};
|
|
745
|
+
/**
|
|
746
|
+
* 将某个文件从【暂存区】移回【工作区】
|
|
747
|
+
* - 保留文件修改
|
|
748
|
+
* - 仅取消 git add 的效果
|
|
749
|
+
*
|
|
750
|
+
* 等价命令:git restore --staged <file>
|
|
751
|
+
*/
|
|
752
|
+
const unstageFile = async (file) => {
|
|
753
|
+
await runGit([
|
|
754
|
+
"restore",
|
|
755
|
+
"--staged",
|
|
756
|
+
file
|
|
757
|
+
]);
|
|
758
|
+
};
|
|
759
|
+
/**
|
|
760
|
+
* 取消所有文件的暂存状态
|
|
761
|
+
* - 保留所有文件修改
|
|
762
|
+
* - 清空暂存区
|
|
763
|
+
*
|
|
764
|
+
* 等价命令:git restore --staged .
|
|
765
|
+
*/
|
|
766
|
+
const unstageAll = async () => {
|
|
767
|
+
await runGit([
|
|
768
|
+
"restore",
|
|
769
|
+
"--staged",
|
|
770
|
+
"."
|
|
771
|
+
]);
|
|
772
|
+
};
|
|
773
|
+
/**
|
|
774
|
+
* 丢弃某个文件的所有修改(暂存 + 未暂存)
|
|
775
|
+
* - 先取消暂存
|
|
776
|
+
* - 再撤销工作区修改
|
|
777
|
+
* - ⚠️ 修改内容将彻底丢失
|
|
778
|
+
*
|
|
779
|
+
* 等价操作:
|
|
780
|
+
* git restore --staged <file>
|
|
781
|
+
* git restore <file>
|
|
782
|
+
*/
|
|
783
|
+
const discardFile = async (file) => {
|
|
784
|
+
await unstageFile(file);
|
|
785
|
+
await restoreFile(file);
|
|
786
|
+
};
|
|
787
|
+
/**
|
|
788
|
+
* 丢弃所有修改(包括暂存和未暂存的文件)
|
|
789
|
+
*
|
|
790
|
+
* 作用:
|
|
791
|
+
* - 将暂存区和工作区全部重置到当前 HEAD 提交状态
|
|
792
|
+
* - 丢弃所有未提交的更改,无法恢复(除非使用 reflog)
|
|
793
|
+
*
|
|
794
|
+
* ⚠️ 高风险操作,请确保已备份重要修改
|
|
795
|
+
*
|
|
796
|
+
* 等价命令:
|
|
797
|
+
* git reset --hard HEAD
|
|
798
|
+
*/
|
|
799
|
+
const discardAll = async () => {
|
|
800
|
+
await runGit([
|
|
801
|
+
"reset",
|
|
802
|
+
"--hard",
|
|
803
|
+
"HEAD"
|
|
804
|
+
]);
|
|
805
|
+
};
|
|
806
|
+
/**
|
|
807
|
+
* 撤销最近的 commit(软回退)
|
|
808
|
+
* - 提交历史回退
|
|
809
|
+
* - 修改内容保留
|
|
810
|
+
* - 修改仍在【暂存区】
|
|
811
|
+
*
|
|
812
|
+
* 常用于:刚提交但还想改点东西
|
|
813
|
+
*
|
|
814
|
+
* 等价命令:git reset --soft HEAD~<count>
|
|
815
|
+
*/
|
|
816
|
+
const resetSoft = async (count = 1) => {
|
|
817
|
+
await runGit([
|
|
818
|
+
"reset",
|
|
819
|
+
"--soft",
|
|
820
|
+
`HEAD~${count}`
|
|
821
|
+
]);
|
|
822
|
+
};
|
|
823
|
+
/**
|
|
824
|
+
* 撤销最近的 commit(混合回退,默认行为)
|
|
825
|
+
* - 提交历史回退
|
|
826
|
+
* - 修改内容保留
|
|
827
|
+
* - 修改回到【工作区】,不再暂存
|
|
828
|
+
*
|
|
829
|
+
* 等价命令:git reset --mixed HEAD~<count>
|
|
830
|
+
*/
|
|
831
|
+
const resetMixed = async (count = 1) => {
|
|
832
|
+
await runGit([
|
|
833
|
+
"reset",
|
|
834
|
+
"--mixed",
|
|
835
|
+
`HEAD~${count}`
|
|
836
|
+
]);
|
|
837
|
+
};
|
|
838
|
+
/**
|
|
839
|
+
* 撤销最近的 commit(强制回退)
|
|
840
|
+
* - 提交历史回退
|
|
841
|
+
* - ⚠️ 所有修改全部丢弃
|
|
842
|
+
*
|
|
843
|
+
* ⚠️ 高危操作,请谨慎使用
|
|
844
|
+
*
|
|
845
|
+
* 等价命令:git reset --hard HEAD~<count>
|
|
846
|
+
*/
|
|
847
|
+
const resetHard = async (count = 1) => {
|
|
848
|
+
await runGit([
|
|
849
|
+
"reset",
|
|
850
|
+
"--hard",
|
|
851
|
+
`HEAD~${count}`
|
|
852
|
+
]);
|
|
853
|
+
};
|
|
854
|
+
/**
|
|
855
|
+
* 安全撤销一个已提交(并可能已 push)的 commit
|
|
856
|
+
* - 不改写提交历史
|
|
857
|
+
* - 生成一个新的反向提交
|
|
858
|
+
*
|
|
859
|
+
* 适用于:公共分支 / 已推送到远端
|
|
860
|
+
*
|
|
861
|
+
* 等价命令:git revert <commit>
|
|
862
|
+
*/
|
|
863
|
+
const revertCommit = async (hash) => {
|
|
864
|
+
await runGit(["revert", hash]);
|
|
865
|
+
};
|
|
866
|
+
/**
|
|
867
|
+
* 将某个文件恢复到指定 commit 的状态
|
|
868
|
+
* - 仅影响该文件
|
|
869
|
+
* - 会覆盖当前工作区中的该文件
|
|
870
|
+
* - 不自动提交
|
|
871
|
+
*
|
|
872
|
+
* 等价命令:git restore --source=<commit> <file>
|
|
873
|
+
*/
|
|
874
|
+
const restoreFileFromCommit = async (file, commit) => {
|
|
875
|
+
await runGit([
|
|
876
|
+
"restore",
|
|
877
|
+
`--source=${commit}`,
|
|
878
|
+
file
|
|
879
|
+
]);
|
|
880
|
+
};
|
|
881
|
+
/**
|
|
882
|
+
* 将当前目录下的所有文件恢复到指定 commit 的状态
|
|
883
|
+
*
|
|
884
|
+
* 行为说明:
|
|
885
|
+
* - 仅影响【工作区(Working Tree)】
|
|
886
|
+
* - 不影响暂存区(Index)
|
|
887
|
+
* - 不修改提交历史
|
|
888
|
+
*
|
|
889
|
+
* ⚠️ 风险提示:
|
|
890
|
+
* - 会覆盖当前工作区中的所有文件
|
|
891
|
+
* - 所有未暂存的修改将被永久丢弃
|
|
892
|
+
*
|
|
893
|
+
* 适用场景:
|
|
894
|
+
* - 批量回退文件内容到某个历史版本
|
|
895
|
+
* - 修复误操作、误格式化、误生成文件等情况
|
|
896
|
+
*
|
|
897
|
+
* 等价命令:
|
|
898
|
+
* git restore --source=<commit> .
|
|
899
|
+
*/
|
|
900
|
+
const restoreFromCommit = async (commit) => {
|
|
901
|
+
await runGit([
|
|
902
|
+
"restore",
|
|
903
|
+
`--source=${commit}`,
|
|
904
|
+
"."
|
|
905
|
+
]);
|
|
906
|
+
};
|
|
907
|
+
const deleteTag = async (tag) => {
|
|
908
|
+
await runGit([
|
|
909
|
+
"tag",
|
|
910
|
+
"--delete",
|
|
911
|
+
tag
|
|
912
|
+
]);
|
|
913
|
+
};
|
|
914
|
+
/** {@link restoreAll} 的同步版本 */
|
|
915
|
+
const restoreAllSync = async () => {
|
|
916
|
+
runGitSync(["restore", "."]);
|
|
917
|
+
};
|
|
918
|
+
const deleteTagSync = async (tag) => {
|
|
919
|
+
runGitSync([
|
|
920
|
+
"tag",
|
|
921
|
+
"--delete",
|
|
922
|
+
tag
|
|
923
|
+
]);
|
|
924
|
+
};
|
|
925
|
+
const discardAllSync = async () => {
|
|
926
|
+
runGitSync([
|
|
927
|
+
"reset",
|
|
928
|
+
"--hard",
|
|
929
|
+
"HEAD"
|
|
930
|
+
]);
|
|
931
|
+
};
|
|
932
|
+
const resetHardSync = async (count = 1) => {
|
|
933
|
+
runGitSync([
|
|
934
|
+
"reset",
|
|
935
|
+
"--hard",
|
|
936
|
+
`HEAD~${count}`
|
|
937
|
+
]);
|
|
938
|
+
};
|
|
939
|
+
const gitUndo = {
|
|
940
|
+
file: {
|
|
941
|
+
restore: restoreFile,
|
|
942
|
+
unstage: unstageFile,
|
|
943
|
+
discard: discardFile
|
|
944
|
+
},
|
|
945
|
+
all: {
|
|
946
|
+
restore: restoreAll,
|
|
947
|
+
unstage: unstageAll,
|
|
948
|
+
discard: discardAll
|
|
949
|
+
},
|
|
950
|
+
reset: {
|
|
951
|
+
soft: resetSoft,
|
|
952
|
+
mixed: resetMixed,
|
|
953
|
+
hard: resetHard
|
|
954
|
+
},
|
|
955
|
+
revert: revertCommit
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
//#endregion
|
|
959
|
+
//#region src/npm.ts
|
|
960
|
+
const DEFAULT_TAG = "latest";
|
|
961
|
+
const DEFAULT_ACCESS = "public";
|
|
962
|
+
const DEFAULT_REGISTRY = "https://registry.npmjs.org/";
|
|
963
|
+
const accessArg = (access = DEFAULT_ACCESS) => {
|
|
964
|
+
return ["--access", access];
|
|
965
|
+
};
|
|
966
|
+
const registryArg = (registry = DEFAULT_REGISTRY) => {
|
|
967
|
+
return ["--registry", registry];
|
|
968
|
+
};
|
|
969
|
+
const tagArg = (tag = DEFAULT_TAG) => {
|
|
970
|
+
return ["--tag", tag];
|
|
971
|
+
};
|
|
972
|
+
/**
|
|
973
|
+
* 获取 npm registry
|
|
974
|
+
* @param {string} pkgDir
|
|
975
|
+
* @returns {Promise<string>}
|
|
976
|
+
* @defaults https://registry.npmjs.org/
|
|
977
|
+
*/
|
|
978
|
+
const getRegistry = async (pkgDir) => {
|
|
979
|
+
const n = (r) => r.endsWith("/") ? r : `${r}/`;
|
|
980
|
+
const { publishConfig = {} } = readJsonFile((0, node_path.join)(pkgDir, "package.json"));
|
|
981
|
+
const { registry } = publishConfig;
|
|
982
|
+
if (registry) return n(registry);
|
|
983
|
+
const res = await runNpm([
|
|
984
|
+
"config",
|
|
985
|
+
"get",
|
|
986
|
+
"registry"
|
|
987
|
+
], { cwd: pkgDir });
|
|
988
|
+
if (res) return n(res);
|
|
989
|
+
return "https://registry.npmjs.org/";
|
|
990
|
+
};
|
|
991
|
+
/**
|
|
992
|
+
* 获取 publish access
|
|
993
|
+
* @param {string} pkgDir
|
|
994
|
+
* @returns {Promise<string>}
|
|
995
|
+
* @defaults scoped: restricted; unscoped: public
|
|
996
|
+
*/
|
|
997
|
+
const getAccess = async (pkgDir) => {
|
|
998
|
+
const { name, publishConfig = {} } = readJsonFile((0, node_path.join)(pkgDir, "package.json"));
|
|
999
|
+
const { access } = publishConfig;
|
|
1000
|
+
return access || (name.startsWith("@") ? "restricted" : "public");
|
|
1001
|
+
};
|
|
1002
|
+
/**
|
|
1003
|
+
* 检查与仓库的连接
|
|
1004
|
+
* @param {string} registry
|
|
1005
|
+
* @returns {Promise<boolean>}
|
|
1006
|
+
* @defaults npm ping --registry https://registry.npmjs.org/
|
|
1007
|
+
*/
|
|
1008
|
+
const pingRegistry = async (registry) => {
|
|
1009
|
+
return void 0 !== await runNpm(["ping", ...registryArg(registry)]);
|
|
1010
|
+
};
|
|
1011
|
+
/**
|
|
1012
|
+
* 获取已登录用户
|
|
1013
|
+
* @param {string} registry
|
|
1014
|
+
* @returns {Promise<string | undefined>}
|
|
1015
|
+
* @defaults npm whoami --registry https://registry.npmjs.org/
|
|
1016
|
+
*/
|
|
1017
|
+
const getAuthenticatedUser = (registry) => {
|
|
1018
|
+
return runNpm(["whoami", ...registryArg(registry)]);
|
|
1019
|
+
};
|
|
1020
|
+
/**
|
|
1021
|
+
* 用户是否拥有写入权限
|
|
1022
|
+
* @param {string} pkg
|
|
1023
|
+
* @param {string} user
|
|
1024
|
+
* @param {string} registry
|
|
1025
|
+
* @returns {Promise<boolean>}
|
|
1026
|
+
* @defaults npm access list collaborators <pkg> --json
|
|
1027
|
+
* npm access ls-collaborators <pkg> --json
|
|
1028
|
+
*/
|
|
1029
|
+
const hasWriteAccess = async (pkg, user, registry) => {
|
|
1030
|
+
const res = await runNpm([
|
|
1031
|
+
"access",
|
|
1032
|
+
"list",
|
|
1033
|
+
"collaborators",
|
|
1034
|
+
pkg,
|
|
1035
|
+
"--json",
|
|
1036
|
+
...registryArg(registry)
|
|
1037
|
+
]).catch((_) => runNpm([
|
|
1038
|
+
"access",
|
|
1039
|
+
"ls-collaborators",
|
|
1040
|
+
pkg,
|
|
1041
|
+
"--json",
|
|
1042
|
+
...registryArg(registry)
|
|
1043
|
+
]));
|
|
1044
|
+
return (JSON.parse(res ?? "{}")[user] ?? "").includes("read-write");
|
|
1045
|
+
};
|
|
1046
|
+
/**
|
|
1047
|
+
* 获取指定包的版本
|
|
1048
|
+
* @param {string} pkg pkgName pkgName@tag
|
|
1049
|
+
* @param {string} registry
|
|
1050
|
+
* @returns {Promise<string | undefined>}
|
|
1051
|
+
* @defaults npm view <pkg> version
|
|
1052
|
+
*/
|
|
1053
|
+
const getPublishedVersion = (pkg, registry) => {
|
|
1054
|
+
return runNpm([
|
|
1055
|
+
"view",
|
|
1056
|
+
pkg,
|
|
1057
|
+
"version",
|
|
1058
|
+
...registryArg(registry)
|
|
1059
|
+
]);
|
|
1060
|
+
};
|
|
1061
|
+
/**
|
|
1062
|
+
* 获取所有已发布的 dist-tags
|
|
1063
|
+
* @param {string} pkg
|
|
1064
|
+
* @param {string} registry
|
|
1065
|
+
* @returns {Promise<string[]>}
|
|
1066
|
+
* @defaults npm view <pkg> dist-tags --json
|
|
1067
|
+
*/
|
|
1068
|
+
const getDistTags = async (pkg, registry) => {
|
|
1069
|
+
const res = await runNpm([
|
|
1070
|
+
"view",
|
|
1071
|
+
pkg,
|
|
1072
|
+
"dist-tags",
|
|
1073
|
+
"--json",
|
|
1074
|
+
...registryArg(registry)
|
|
1075
|
+
]);
|
|
1076
|
+
return Object.keys(JSON.parse(res || "{}"));
|
|
1077
|
+
};
|
|
1078
|
+
/**
|
|
1079
|
+
* 更新包版本号
|
|
1080
|
+
* @param {string} version
|
|
1081
|
+
* @param {string[]} args
|
|
1082
|
+
* @param {string} cwd
|
|
1083
|
+
* @returns {Promise<string | undefined>}
|
|
1084
|
+
* @defaults npm version <version> --workspaces=false --no-git-tag-version --allow-same-version
|
|
1085
|
+
*/
|
|
1086
|
+
const bumpPackageVersion = (version, args = [], cwd = ".") => {
|
|
1087
|
+
return runNpm([
|
|
1088
|
+
"version",
|
|
1089
|
+
version,
|
|
1090
|
+
"--workspaces=false",
|
|
1091
|
+
"--no-git-tag-version",
|
|
1092
|
+
"--allow-same-version",
|
|
1093
|
+
...args
|
|
1094
|
+
], { cwd });
|
|
1095
|
+
};
|
|
1096
|
+
/**
|
|
1097
|
+
* 发布
|
|
1098
|
+
* @param {{access?: string, registry?: string, tag?: string, args?: string[], cwd?: string}} options
|
|
1099
|
+
* @returns {Promise<string | undefined>}
|
|
1100
|
+
* @defaults npm publish --tag latest --access public --registry https://registry.npmjs.org/ --workspaces=false
|
|
1101
|
+
*/
|
|
1102
|
+
const publishPackage = (options) => {
|
|
1103
|
+
const { tag, access, registry, args = [], cwd = "." } = options ?? {};
|
|
1104
|
+
return runNpm([
|
|
1105
|
+
"publish",
|
|
1106
|
+
...tagArg(tag),
|
|
1107
|
+
...accessArg(access),
|
|
1108
|
+
...registryArg(registry),
|
|
1109
|
+
"--workspaces=false",
|
|
1110
|
+
...args
|
|
1111
|
+
], { cwd });
|
|
1112
|
+
};
|
|
1113
|
+
|
|
1114
|
+
//#endregion
|
|
1115
|
+
exports.ConfirmResult = ConfirmResult;
|
|
1116
|
+
exports.DEFAULT_ACCESS = DEFAULT_ACCESS;
|
|
1117
|
+
exports.DEFAULT_REGISTRY = DEFAULT_REGISTRY;
|
|
1118
|
+
exports.DEFAULT_TAG = DEFAULT_TAG;
|
|
1119
|
+
exports.HttpLibrary = HttpLibrary;
|
|
1120
|
+
exports.PkgManager = PkgManager;
|
|
1121
|
+
exports.YesOrNo = YesOrNo;
|
|
1122
|
+
exports.accessArg = accessArg;
|
|
1123
|
+
exports.bumpPackageVersion = bumpPackageVersion;
|
|
1124
|
+
exports.checkVersion = checkVersion;
|
|
1125
|
+
exports.coloredChangeset = coloredChangeset;
|
|
1126
|
+
exports.copyDirAsync = copyDirAsync;
|
|
1127
|
+
exports.countCommitsSinceLatestTag = countCommitsSinceLatestTag;
|
|
1128
|
+
exports.deleteTag = deleteTag;
|
|
1129
|
+
exports.deleteTagSync = deleteTagSync;
|
|
1130
|
+
exports.discardAll = discardAll;
|
|
1131
|
+
exports.discardAllSync = discardAllSync;
|
|
1132
|
+
exports.discardFile = discardFile;
|
|
1133
|
+
exports.editFile = editFile;
|
|
1134
|
+
exports.editJsonFile = editJsonFile;
|
|
1135
|
+
exports.emptyDir = emptyDir;
|
|
1136
|
+
exports.eol = eol;
|
|
1137
|
+
exports.execAsync = execAsync;
|
|
1138
|
+
exports.execSyncWithString = execSyncWithString;
|
|
1139
|
+
exports.fetchAllBranch = fetchAllBranch;
|
|
1140
|
+
exports.getAccess = getAccess;
|
|
1141
|
+
exports.getAllRemotes = getAllRemotes;
|
|
1142
|
+
exports.getAuthenticatedUser = getAuthenticatedUser;
|
|
1143
|
+
exports.getCurrentBranch = getCurrentBranch;
|
|
1144
|
+
exports.getDefaultRemote = getDefaultRemote;
|
|
1145
|
+
exports.getDistTags = getDistTags;
|
|
1146
|
+
exports.getFullHash = getFullHash;
|
|
1147
|
+
exports.getGitConfig = getGitConfig;
|
|
1148
|
+
exports.getGitRemoteUrl = getGitRemoteUrl;
|
|
1149
|
+
exports.getGithubReleaseUrl = getGithubReleaseUrl;
|
|
1150
|
+
exports.getGithubUrl = getGithubUrl;
|
|
1151
|
+
exports.getLatestTag = getLatestTag;
|
|
1152
|
+
exports.getLocalTags = getLocalTags;
|
|
1153
|
+
exports.getLog = getLog;
|
|
1154
|
+
exports.getOtherRemotes = getOtherRemotes;
|
|
1155
|
+
exports.getPackageInfo = getPackageInfo;
|
|
1156
|
+
exports.getPackageUrl = getPackageUrl;
|
|
1157
|
+
exports.getPreviousTag = getPreviousTag;
|
|
1158
|
+
exports.getPublishedVersion = getPublishedVersion;
|
|
1159
|
+
exports.getRegistry = getRegistry;
|
|
1160
|
+
exports.getRemote = getRemote;
|
|
1161
|
+
exports.getRemoteForBranch = getRemoteForBranch;
|
|
1162
|
+
exports.getRemoteTags = getRemoteTags;
|
|
1163
|
+
exports.getSortedTags = getSortedTags;
|
|
1164
|
+
exports.getStatus = getStatus;
|
|
1165
|
+
exports.getUpstreamArgs = getUpstreamArgs;
|
|
1166
|
+
exports.gitAddAll = gitAddAll;
|
|
1167
|
+
exports.gitAddTracked = gitAddTracked;
|
|
1168
|
+
exports.gitCommit = gitCommit;
|
|
1169
|
+
exports.gitCommitAmend = gitCommitAmend;
|
|
1170
|
+
exports.gitTagAnnotated = gitTagAnnotated;
|
|
1171
|
+
exports.gitTagLightweight = gitTagLightweight;
|
|
1172
|
+
exports.gitUndo = gitUndo;
|
|
1173
|
+
exports.hasUpstreamBranch = hasUpstreamBranch;
|
|
1174
|
+
exports.hasWriteAccess = hasWriteAccess;
|
|
1175
|
+
exports.isEmpty = isEmpty;
|
|
1176
|
+
exports.isGitRepo = isGitRepo;
|
|
1177
|
+
exports.isRemoteName = isRemoteName;
|
|
1178
|
+
exports.isTestFile = isTestFile;
|
|
1179
|
+
exports.isValidPackageName = isValidPackageName;
|
|
1180
|
+
exports.isWorkingDirClean = isWorkingDirClean;
|
|
1181
|
+
exports.joinUrl = joinUrl;
|
|
1182
|
+
exports.parseArgs = parseArgs;
|
|
1183
|
+
exports.parseGitHubRepo = parseGitHubRepo;
|
|
1184
|
+
exports.pingRegistry = pingRegistry;
|
|
1185
|
+
exports.pkgFromUserAgent = pkgFromUserAgent;
|
|
1186
|
+
exports.publishPackage = publishPackage;
|
|
1187
|
+
exports.pushBranch = pushBranch;
|
|
1188
|
+
exports.pushTag = pushTag;
|
|
1189
|
+
exports.readJsonFile = readJsonFile;
|
|
1190
|
+
exports.readSubDirs = readSubDirs;
|
|
1191
|
+
exports.registryArg = registryArg;
|
|
1192
|
+
exports.resetHard = resetHard;
|
|
1193
|
+
exports.resetHardSync = resetHardSync;
|
|
1194
|
+
exports.resetMixed = resetMixed;
|
|
1195
|
+
exports.resetSoft = resetSoft;
|
|
1196
|
+
exports.resolveChangelogRange = resolveChangelogRange;
|
|
1197
|
+
exports.restoreAll = restoreAll;
|
|
1198
|
+
exports.restoreAllSync = restoreAllSync;
|
|
1199
|
+
exports.restoreFile = restoreFile;
|
|
1200
|
+
exports.restoreFileFromCommit = restoreFileFromCommit;
|
|
1201
|
+
exports.restoreFromCommit = restoreFromCommit;
|
|
1202
|
+
exports.revertCommit = revertCommit;
|
|
1203
|
+
exports.runCliForTest = runCliForTest;
|
|
1204
|
+
exports.runGit = runGit;
|
|
1205
|
+
exports.runGitSync = runGitSync;
|
|
1206
|
+
exports.runNode = runNode;
|
|
1207
|
+
exports.runNodeSync = runNodeSync;
|
|
1208
|
+
exports.runNpm = runNpm;
|
|
1209
|
+
exports.runNpmSync = runNpmSync;
|
|
1210
|
+
exports.space = space;
|
|
1211
|
+
exports.spawnAsync = spawnAsync;
|
|
1212
|
+
exports.spawnSyncWithString = spawnSyncWithString;
|
|
1213
|
+
exports.stringifyArgs = stringifyArgs;
|
|
1214
|
+
exports.tagArg = tagArg;
|
|
1215
|
+
exports.toValidPackageName = toValidPackageName;
|
|
1216
|
+
exports.toValidProjectName = toValidProjectName;
|
|
1217
|
+
exports.trimTemplate = trimTemplate;
|
|
1218
|
+
exports.unstageAll = unstageAll;
|
|
1219
|
+
exports.unstageFile = unstageFile;
|