@peiyanlu/cli-utils 0.0.3 → 0.0.5

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 ADDED
@@ -0,0 +1,1237 @@
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
438
+ */
439
+ const getTags = async () => {
440
+ const tags = await runGit(["tag"]);
441
+ return tags ? tags.split("\n").sort() : [];
442
+ };
443
+ /**
444
+ * 获取远程(或所有 refs)中的 tag
445
+ * 使用 for-each-ref 以支持版本号排序
446
+ * @param {string} match 默认 *
447
+ * @returns {Promise<string[]>}
448
+ * @defaults git for-each-ref --sort=-v:refname --format=%(refname:short) refs/tags/<match>
449
+ */
450
+ const getRemoteTags = async (match = "*") => {
451
+ const tags = await runGit([
452
+ "for-each-ref",
453
+ "--sort=-v:refname",
454
+ "--format=%(refname:short)",
455
+ `refs/tags/${match}`
456
+ ]);
457
+ return tags ? tags.split("\n").sort() : [];
458
+ };
459
+ /**
460
+ * 获取当前工作区状态(不包含未跟踪文件)
461
+ * @returns {Promise<string | undefined>}
462
+ * @defaults git status --short --untracked-files=no
463
+ */
464
+ const getStatus = () => {
465
+ return runGit([
466
+ "status",
467
+ "--short",
468
+ "--untracked-files=no"
469
+ ], { trim: false });
470
+ };
471
+ /**
472
+ * 为 git status / changeset 输出添加颜色
473
+ *
474
+ * M -> 黄色(修改)
475
+ * A -> 绿色(新增)
476
+ * D -> 红色(删除)
477
+ * @param {string} log git status --short 输出
478
+ * @returns {string}
479
+ */
480
+ const coloredChangeset = (log) => {
481
+ const colorStatusChar = (ch) => {
482
+ switch (ch) {
483
+ case "M": return `\x1b[33m${ch}\x1b[0m\x1b[2m`;
484
+ case "A": return `\x1b[32m${ch}\x1b[0m\x1b[2m`;
485
+ case "D": return `\x1b[31m${ch}\x1b[0m\x1b[2m`;
486
+ default: return ch;
487
+ }
488
+ };
489
+ const colorStatus = (status) => status.split("").map(colorStatusChar).join("");
490
+ return log.split("\n").filter(Boolean).map((line) => {
491
+ const status = line.slice(0, 2);
492
+ const file = line.slice(3);
493
+ return `${colorStatus(status)} ${file}`;
494
+ }).join("\n");
495
+ };
496
+ /**
497
+ * 获取当前分支对应的远程信息
498
+ * @returns {Promise<{remoteName: string, remoteUrl: string | undefined}>}
499
+ */
500
+ const getRemote = async () => {
501
+ const branch = await getCurrentBranch();
502
+ const remoteName = branch && await getRemoteForBranch(branch) || "origin";
503
+ return {
504
+ remoteName,
505
+ remoteUrl: await runGit([
506
+ "remote",
507
+ "get-url",
508
+ remoteName
509
+ ]).catch((_) => runGit([
510
+ "config",
511
+ "--get",
512
+ `remote.${remoteName}.url`
513
+ ]))
514
+ };
515
+ };
516
+ /**
517
+ * 获取最新 tag(基于 git describe)
518
+ *
519
+ * 默认:
520
+ * - 匹配所有 tag
521
+ * - 排除 beta 版本
522
+ * @param {string} match 默认 *
523
+ * @param {string} exclude 默认 beta
524
+ * @returns {Promise<string | undefined>}
525
+ * @defaults git describe --tags --abbrev=0 --match=<match> --exclude=<exclude>
526
+ */
527
+ const getLatestTag = async (match = "*", exclude = "*-beta.*") => {
528
+ return runGit([
529
+ "describe",
530
+ "--tags",
531
+ "--abbrev=0",
532
+ `--match=${match}`,
533
+ `--exclude=${exclude}`
534
+ ]);
535
+ };
536
+ /**
537
+ * 从所有 refs 中获取最新 tag
538
+ * 适用于 git describe 不可靠的场景(如 tag 不在当前分支)
539
+ * @param {string} match
540
+ * @returns {Promise<string | undefined>}
541
+ * @defaults git -c versionsort.suffix=- for-each-ref --count=1 --sort=-v:refname --format=%(refname:short) refs/tags/<match>
542
+ */
543
+ const getLatestTagFromAllRefs = async (match = "*") => {
544
+ return runGit([
545
+ "-c",
546
+ "versionsort.suffix=-",
547
+ "for-each-ref",
548
+ "--count=1",
549
+ "--sort=-v:refname",
550
+ "--format=%(refname:short)",
551
+ `refs/tags/${match}`
552
+ ]);
553
+ };
554
+ /**
555
+ * 获取上一个 tag
556
+ * @param {string} current
557
+ * @returns {Promise<string | undefined>}
558
+ */
559
+ const getPreviousTag = async (current) => {
560
+ return runGit([
561
+ "describe",
562
+ "--tags",
563
+ "--abbrev=0",
564
+ `${await runGit([
565
+ "rev-list",
566
+ "--tags",
567
+ current ?? "--skip=1",
568
+ "--max-count=1"
569
+ ])}^`
570
+ ]);
571
+ };
572
+ /**
573
+ * 将短 hash 解析为完整 hash
574
+ * @param {string} short
575
+ * @returns {Promise<string | undefined>}
576
+ */
577
+ const getFullHash = (short) => {
578
+ return runGit(["rev-parse", short]);
579
+ };
580
+ /**
581
+ * 计算 changelog 的 commit 范围
582
+ * @param {boolean} isIncrement 是否为版本递增发布
583
+ * @returns {Promise<{from: string, to: string} | {from: string, to: string} | {from: string, to: string}>}
584
+ */
585
+ const resolveChangelogRange = async (isIncrement = true) => {
586
+ const latestTag = await getLatestTag();
587
+ const previousTag = await getPreviousTag(latestTag);
588
+ if (!latestTag) return {
589
+ from: "",
590
+ to: "HEAD"
591
+ };
592
+ if (!isIncrement && previousTag) return {
593
+ from: previousTag,
594
+ to: `${latestTag}^1`
595
+ };
596
+ return {
597
+ from: latestTag,
598
+ to: "HEAD"
599
+ };
600
+ };
601
+ /**
602
+ * 获取指定范围内的 commit 日志
603
+ * @param {string} from
604
+ * @param {string} to
605
+ * @param {string} scope 文件或目录范围
606
+ * @returns {Promise<string | undefined>}
607
+ */
608
+ const getLog = async (from = "", to = "HEAD", scope) => {
609
+ const cmd = ["log", `--pretty=format:* %s (%h)`];
610
+ if (from) cmd.push(`${from}...${to}`);
611
+ if (scope) cmd.push(...["--", scope]);
612
+ return runGit(cmd, { trim: false });
613
+ };
614
+ /**
615
+ * 判断工作区是否干净(无未提交修改)
616
+ * @returns {Promise<boolean>}
617
+ */
618
+ const isWorkingDirClean = async () => {
619
+ return (await runGit(["status", "-s"]))?.length === 0;
620
+ };
621
+ /** 判断字符串是否为合法 remote 名称 */
622
+ const isRemoteName = (remoteName) => {
623
+ return remoteName && !remoteName.includes("/");
624
+ };
625
+ /** 判断当前分支是否已设置 upstream */
626
+ const hasUpstreamBranch = async () => {
627
+ const branch = await runGit([
628
+ "for-each-ref",
629
+ "--format=%(upstream:short)",
630
+ await runGit(["symbolic-ref", "HEAD"])
631
+ ]);
632
+ return Boolean(branch);
633
+ };
634
+ /** 获取 push 所需的 upstream 参数 */
635
+ const getUpstreamArgs = async (remoteName, branch) => {
636
+ const hasUpstream = await hasUpstreamBranch();
637
+ const target = branch ?? await getCurrentBranch();
638
+ if (!hasUpstream) return [
639
+ "--set-upstream",
640
+ remoteName,
641
+ target
642
+ ];
643
+ return [remoteName, target];
644
+ };
645
+ /** 统计自最新 tag 以来的提交数量 */
646
+ const countCommitsSinceLatestTag = async () => {
647
+ const latestTag = await getLatestTag();
648
+ return runGit([
649
+ "rev-list",
650
+ latestTag ? `${latestTag}...HEAD` : "HEAD",
651
+ "--count"
652
+ ]).then(Number);
653
+ };
654
+ /** git add(包含未追踪文件) */
655
+ const gitAddAll = async (args = []) => {
656
+ await runGit([
657
+ "add",
658
+ "--all",
659
+ ...args
660
+ ]);
661
+ };
662
+ /** git add(仅已追踪文件) */
663
+ const gitAddTracked = async (args = []) => {
664
+ await runGit([
665
+ "add",
666
+ "--update",
667
+ ...args
668
+ ]);
669
+ };
670
+ /** git commit */
671
+ const gitCommit = async (message, args = []) => {
672
+ await runGit([
673
+ "commit",
674
+ "--message",
675
+ message,
676
+ ...args
677
+ ]);
678
+ };
679
+ /** git commit amend */
680
+ const gitCommitAmend = async (message, args = []) => {
681
+ await runGit([
682
+ "commit",
683
+ "--amend",
684
+ "--message",
685
+ message,
686
+ ...args
687
+ ]);
688
+ };
689
+ /** 创建 annotated tag */
690
+ const gitTagAnnotated = async (tag, message = tag, args = []) => {
691
+ await runGit([
692
+ "tag",
693
+ "--annotate",
694
+ "--message",
695
+ message,
696
+ tag,
697
+ ...args
698
+ ]);
699
+ };
700
+ /** 创建 lightweight tag */
701
+ const gitTagLightweight = async (tag, message = tag, args = []) => {
702
+ await runGit([
703
+ "tag",
704
+ "--message",
705
+ message,
706
+ tag,
707
+ ...args
708
+ ]);
709
+ };
710
+ /**
711
+ * 推送 tag 到远程
712
+ * @param {string} remoteName
713
+ * @param {string} tag
714
+ * @param {string[]} args
715
+ * @returns {Promise<string | undefined>}
716
+ * @defaults git push <remoteName> refs/tags/<tag>
717
+ */
718
+ const pushTag = async (remoteName, tag, args = []) => {
719
+ await runGit([
720
+ "push",
721
+ remoteName,
722
+ `refs/tags/${tag}`,
723
+ ...args
724
+ ]);
725
+ };
726
+ /**
727
+ * 推送分支到远程
728
+ * 自动处理 upstream 设置
729
+ * @param {string} remoteName
730
+ * @param {string} branch
731
+ * @param {string[]} args
732
+ * @returns {Promise<string | undefined>}
733
+ */
734
+ const pushBranch = async (remoteName, branch, args = []) => {
735
+ await runGit([
736
+ "push",
737
+ ...await getUpstreamArgs(remoteName, branch),
738
+ ...args
739
+ ]);
740
+ };
741
+ /**
742
+ * 撤销【工作区】中某个文件的修改(未暂存的改动)
743
+ * - 不影响暂存区
744
+ * - 不影响提交历史
745
+ * - ⚠️ 会丢弃该文件当前未暂存的修改
746
+ *
747
+ * 等价命令:git restore <file>
748
+ */
749
+ const restoreFile = async (file) => {
750
+ await runGit(["restore", file]);
751
+ };
752
+ /**
753
+ * 撤销【工作区】中所有未暂存的修改
754
+ * - 不影响暂存区
755
+ * - 不影响提交历史
756
+ * - ⚠️ 会丢弃所有未暂存的修改
757
+ *
758
+ * 等价命令:git restore .
759
+ */
760
+ const restoreAll = async () => {
761
+ await runGit(["restore", "."]);
762
+ };
763
+ /**
764
+ * 将某个文件从【暂存区】移回【工作区】
765
+ * - 保留文件修改
766
+ * - 仅取消 git add 的效果
767
+ *
768
+ * 等价命令:git restore --staged <file>
769
+ */
770
+ const unstageFile = async (file) => {
771
+ await runGit([
772
+ "restore",
773
+ "--staged",
774
+ file
775
+ ]);
776
+ };
777
+ /**
778
+ * 取消所有文件的暂存状态
779
+ * - 保留所有文件修改
780
+ * - 清空暂存区
781
+ *
782
+ * 等价命令:git restore --staged .
783
+ */
784
+ const unstageAll = async () => {
785
+ await runGit([
786
+ "restore",
787
+ "--staged",
788
+ "."
789
+ ]);
790
+ };
791
+ /**
792
+ * 丢弃某个文件的所有修改(暂存 + 未暂存)
793
+ * - 先取消暂存
794
+ * - 再撤销工作区修改
795
+ * - ⚠️ 修改内容将彻底丢失
796
+ *
797
+ * 等价操作:
798
+ * git restore --staged <file>
799
+ * git restore <file>
800
+ */
801
+ const discardFile = async (file) => {
802
+ await unstageFile(file);
803
+ await restoreFile(file);
804
+ };
805
+ /**
806
+ * 丢弃所有修改(包括暂存和未暂存的文件)
807
+ *
808
+ * 作用:
809
+ * - 将暂存区和工作区全部重置到当前 HEAD 提交状态
810
+ * - 丢弃所有未提交的更改,无法恢复(除非使用 reflog)
811
+ *
812
+ * ⚠️ 高风险操作,请确保已备份重要修改
813
+ *
814
+ * 等价命令:
815
+ * git reset --hard HEAD
816
+ */
817
+ const discardAll = async () => {
818
+ await runGit([
819
+ "reset",
820
+ "--hard",
821
+ "HEAD"
822
+ ]);
823
+ };
824
+ /**
825
+ * 撤销最近的 commit(软回退)
826
+ * - 提交历史回退
827
+ * - 修改内容保留
828
+ * - 修改仍在【暂存区】
829
+ *
830
+ * 常用于:刚提交但还想改点东西
831
+ *
832
+ * 等价命令:git reset --soft HEAD~<count>
833
+ */
834
+ const resetSoft = async (count = 1) => {
835
+ await runGit([
836
+ "reset",
837
+ "--soft",
838
+ `HEAD~${count}`
839
+ ]);
840
+ };
841
+ /**
842
+ * 撤销最近的 commit(混合回退,默认行为)
843
+ * - 提交历史回退
844
+ * - 修改内容保留
845
+ * - 修改回到【工作区】,不再暂存
846
+ *
847
+ * 等价命令:git reset --mixed HEAD~<count>
848
+ */
849
+ const resetMixed = async (count = 1) => {
850
+ await runGit([
851
+ "reset",
852
+ "--mixed",
853
+ `HEAD~${count}`
854
+ ]);
855
+ };
856
+ /**
857
+ * 撤销最近的 commit(强制回退)
858
+ * - 提交历史回退
859
+ * - ⚠️ 所有修改全部丢弃
860
+ *
861
+ * ⚠️ 高危操作,请谨慎使用
862
+ *
863
+ * 等价命令:git reset --hard HEAD~<count>
864
+ */
865
+ const resetHard = async (count = 1) => {
866
+ await runGit([
867
+ "reset",
868
+ "--hard",
869
+ `HEAD~${count}`
870
+ ]);
871
+ };
872
+ /**
873
+ * 安全撤销一个已提交(并可能已 push)的 commit
874
+ * - 不改写提交历史
875
+ * - 生成一个新的反向提交
876
+ *
877
+ * 适用于:公共分支 / 已推送到远端
878
+ *
879
+ * 等价命令:git revert <commit>
880
+ */
881
+ const revertCommit = async (hash) => {
882
+ await runGit(["revert", hash]);
883
+ };
884
+ /**
885
+ * 将某个文件恢复到指定 commit 的状态
886
+ * - 仅影响该文件
887
+ * - 会覆盖当前工作区中的该文件
888
+ * - 不自动提交
889
+ *
890
+ * 等价命令:git restore --source=<commit> <file>
891
+ */
892
+ const restoreFileFromCommit = async (file, commit) => {
893
+ await runGit([
894
+ "restore",
895
+ `--source=${commit}`,
896
+ file
897
+ ]);
898
+ };
899
+ /**
900
+ * 将当前目录下的所有文件恢复到指定 commit 的状态
901
+ *
902
+ * 行为说明:
903
+ * - 仅影响【工作区(Working Tree)】
904
+ * - 不影响暂存区(Index)
905
+ * - 不修改提交历史
906
+ *
907
+ * ⚠️ 风险提示:
908
+ * - 会覆盖当前工作区中的所有文件
909
+ * - 所有未暂存的修改将被永久丢弃
910
+ *
911
+ * 适用场景:
912
+ * - 批量回退文件内容到某个历史版本
913
+ * - 修复误操作、误格式化、误生成文件等情况
914
+ *
915
+ * 等价命令:
916
+ * git restore --source=<commit> .
917
+ */
918
+ const restoreFromCommit = async (commit) => {
919
+ await runGit([
920
+ "restore",
921
+ `--source=${commit}`,
922
+ "."
923
+ ]);
924
+ };
925
+ const deleteTag = async (tag) => {
926
+ await runGit([
927
+ "tag",
928
+ "--delete",
929
+ tag
930
+ ]);
931
+ };
932
+ /** {@link restoreAll} 的同步版本 */
933
+ const restoreAllSync = async () => {
934
+ runGitSync(["restore", "."]);
935
+ };
936
+ const deleteTagSync = async (tag) => {
937
+ runGitSync([
938
+ "tag",
939
+ "--delete",
940
+ tag
941
+ ]);
942
+ };
943
+ const discardAllSync = async () => {
944
+ runGitSync([
945
+ "reset",
946
+ "--hard",
947
+ "HEAD"
948
+ ]);
949
+ };
950
+ const resetHardSync = async (count = 1) => {
951
+ runGitSync([
952
+ "reset",
953
+ "--hard",
954
+ `HEAD~${count}`
955
+ ]);
956
+ };
957
+ const gitUndo = {
958
+ file: {
959
+ restore: restoreFile,
960
+ unstage: unstageFile,
961
+ discard: discardFile
962
+ },
963
+ all: {
964
+ restore: restoreAll,
965
+ unstage: unstageAll,
966
+ discard: discardAll
967
+ },
968
+ reset: {
969
+ soft: resetSoft,
970
+ mixed: resetMixed,
971
+ hard: resetHard
972
+ },
973
+ revert: revertCommit
974
+ };
975
+
976
+ //#endregion
977
+ //#region src/npm.ts
978
+ const DEFAULT_TAG = "latest";
979
+ const DEFAULT_ACCESS = "public";
980
+ const DEFAULT_REGISTRY = "https://registry.npmjs.org/";
981
+ const accessArg = (access = DEFAULT_ACCESS) => {
982
+ return ["--access", access];
983
+ };
984
+ const registryArg = (registry = DEFAULT_REGISTRY) => {
985
+ return ["--registry", registry];
986
+ };
987
+ const tagArg = (tag = DEFAULT_TAG) => {
988
+ return ["--tag", tag];
989
+ };
990
+ /**
991
+ * 获取 npm registry
992
+ * @param {string} pkgDir
993
+ * @returns {Promise<string>}
994
+ * @defaults https://registry.npmjs.org/
995
+ */
996
+ const getRegistry = async (pkgDir) => {
997
+ const n = (r) => r.endsWith("/") ? r : `${r}/`;
998
+ const { publishConfig = {} } = readJsonFile((0, node_path.join)(pkgDir, "package.json"));
999
+ const { registry } = publishConfig;
1000
+ if (registry) return n(registry);
1001
+ const res = await runNpm([
1002
+ "config",
1003
+ "get",
1004
+ "registry"
1005
+ ], { cwd: pkgDir });
1006
+ if (res) return n(res);
1007
+ return "https://registry.npmjs.org/";
1008
+ };
1009
+ /**
1010
+ * 获取 publish access
1011
+ * @param {string} pkgDir
1012
+ * @returns {Promise<string>}
1013
+ * @defaults scoped: restricted; unscoped: public
1014
+ */
1015
+ const getAccess = async (pkgDir) => {
1016
+ const { name, publishConfig = {} } = readJsonFile((0, node_path.join)(pkgDir, "package.json"));
1017
+ const { access } = publishConfig;
1018
+ return access ?? (name.startsWith("@") ? "restricted" : "public");
1019
+ };
1020
+ /**
1021
+ * 检查与仓库的连接
1022
+ * @param {string} registry
1023
+ * @returns {Promise<boolean>}
1024
+ * @defaults npm ping --registry https://registry.npmjs.org/
1025
+ */
1026
+ const pingRegistry = async (registry) => {
1027
+ return void 0 !== await runNpm(["ping", ...registryArg(registry)]);
1028
+ };
1029
+ /**
1030
+ * 获取已登录用户
1031
+ * @param {string} registry
1032
+ * @returns {Promise<string | undefined>}
1033
+ * @defaults npm whoami --registry https://registry.npmjs.org/
1034
+ */
1035
+ const getAuthenticatedUser = (registry) => {
1036
+ return runNpm(["whoami", ...registryArg(registry)]);
1037
+ };
1038
+ /**
1039
+ * 用户是否拥有写入权限
1040
+ * @param {string} pkg
1041
+ * @param {string} user
1042
+ * @param {string} registry
1043
+ * @returns {Promise<boolean>}
1044
+ * @defaults npm access list collaborators <pkg> --json
1045
+ * npm access ls-collaborators <pkg> --json
1046
+ */
1047
+ const hasWriteAccess = async (pkg, user, registry) => {
1048
+ const res = await runNpm([
1049
+ "access",
1050
+ "list",
1051
+ "collaborators",
1052
+ pkg,
1053
+ "--json",
1054
+ ...registryArg(registry)
1055
+ ]).catch((_) => runNpm([
1056
+ "access",
1057
+ "ls-collaborators",
1058
+ pkg,
1059
+ "--json",
1060
+ ...registryArg(registry)
1061
+ ]));
1062
+ return (JSON.parse(res ?? "{}")[user] ?? "").includes("read-write");
1063
+ };
1064
+ /**
1065
+ * 获取指定包的版本
1066
+ * @param {string} pkg pkgName pkgName@tag
1067
+ * @param {string} registry
1068
+ * @returns {Promise<string | undefined>}
1069
+ * @defaults npm view <pkg> version
1070
+ */
1071
+ const getPublishedVersion = (pkg, registry) => {
1072
+ return runNpm([
1073
+ "view",
1074
+ pkg,
1075
+ "version",
1076
+ ...registryArg(registry)
1077
+ ]);
1078
+ };
1079
+ /**
1080
+ * 获取所有已发布的 dist-tags
1081
+ * @param {string} pkg
1082
+ * @param {string} registry
1083
+ * @returns {Promise<string[]>}
1084
+ * @defaults npm view <pkg> dist-tags --json
1085
+ */
1086
+ const getDistTags = async (pkg, registry) => {
1087
+ const res = await runNpm([
1088
+ "view",
1089
+ pkg,
1090
+ "dist-tags",
1091
+ "--json",
1092
+ ...registryArg(registry)
1093
+ ]);
1094
+ return Object.keys(JSON.parse(res || "{}"));
1095
+ };
1096
+ /**
1097
+ * 更新包版本号
1098
+ * @param {string} version
1099
+ * @param {string[]} args
1100
+ * @param {string} cwd
1101
+ * @returns {Promise<string | undefined>}
1102
+ * @defaults npm version <version> --workspaces=false --no-git-tag-version --allow-same-version
1103
+ */
1104
+ const bumpPackageVersion = (version, args = [], cwd = ".") => {
1105
+ return runNpm([
1106
+ "version",
1107
+ version,
1108
+ "--workspaces=false",
1109
+ "--no-git-tag-version",
1110
+ "--allow-same-version",
1111
+ ...args
1112
+ ], { cwd });
1113
+ };
1114
+ /**
1115
+ * 发布
1116
+ * @param {{access?: string, registry?: string, tag?: string, args?: string[], cwd?: string}} options
1117
+ * @returns {Promise<string | undefined>}
1118
+ * @defaults npm publish --tag latest --access public --registry https://registry.npmjs.org/ --workspaces=false
1119
+ */
1120
+ const publishPackage = (options) => {
1121
+ const { tag, access, registry, args = [], cwd = "." } = options ?? {};
1122
+ return runNpm([
1123
+ "publish",
1124
+ ...tagArg(tag),
1125
+ ...accessArg(access),
1126
+ ...registryArg(registry),
1127
+ "--workspaces=false",
1128
+ ...args
1129
+ ], { cwd });
1130
+ };
1131
+
1132
+ //#endregion
1133
+ exports.ConfirmResult = ConfirmResult;
1134
+ exports.DEFAULT_ACCESS = DEFAULT_ACCESS;
1135
+ exports.DEFAULT_REGISTRY = DEFAULT_REGISTRY;
1136
+ exports.DEFAULT_TAG = DEFAULT_TAG;
1137
+ exports.HttpLibrary = HttpLibrary;
1138
+ exports.PkgManager = PkgManager;
1139
+ exports.YesOrNo = YesOrNo;
1140
+ exports.accessArg = accessArg;
1141
+ exports.bumpPackageVersion = bumpPackageVersion;
1142
+ exports.checkVersion = checkVersion;
1143
+ exports.coloredChangeset = coloredChangeset;
1144
+ exports.copyDirAsync = copyDirAsync;
1145
+ exports.countCommitsSinceLatestTag = countCommitsSinceLatestTag;
1146
+ exports.deleteTag = deleteTag;
1147
+ exports.deleteTagSync = deleteTagSync;
1148
+ exports.discardAll = discardAll;
1149
+ exports.discardAllSync = discardAllSync;
1150
+ exports.discardFile = discardFile;
1151
+ exports.editFile = editFile;
1152
+ exports.editJsonFile = editJsonFile;
1153
+ exports.emptyDir = emptyDir;
1154
+ exports.eol = eol;
1155
+ exports.execAsync = execAsync;
1156
+ exports.execSyncWithString = execSyncWithString;
1157
+ exports.fetchAllBranch = fetchAllBranch;
1158
+ exports.getAccess = getAccess;
1159
+ exports.getAllRemotes = getAllRemotes;
1160
+ exports.getAuthenticatedUser = getAuthenticatedUser;
1161
+ exports.getCurrentBranch = getCurrentBranch;
1162
+ exports.getDefaultRemote = getDefaultRemote;
1163
+ exports.getDistTags = getDistTags;
1164
+ exports.getFullHash = getFullHash;
1165
+ exports.getGitConfig = getGitConfig;
1166
+ exports.getGitRemoteUrl = getGitRemoteUrl;
1167
+ exports.getGithubReleaseUrl = getGithubReleaseUrl;
1168
+ exports.getGithubUrl = getGithubUrl;
1169
+ exports.getLatestTag = getLatestTag;
1170
+ exports.getLatestTagFromAllRefs = getLatestTagFromAllRefs;
1171
+ exports.getLog = getLog;
1172
+ exports.getOtherRemotes = getOtherRemotes;
1173
+ exports.getPackageInfo = getPackageInfo;
1174
+ exports.getPackageUrl = getPackageUrl;
1175
+ exports.getPreviousTag = getPreviousTag;
1176
+ exports.getPublishedVersion = getPublishedVersion;
1177
+ exports.getRegistry = getRegistry;
1178
+ exports.getRemote = getRemote;
1179
+ exports.getRemoteForBranch = getRemoteForBranch;
1180
+ exports.getRemoteTags = getRemoteTags;
1181
+ exports.getStatus = getStatus;
1182
+ exports.getTags = getTags;
1183
+ exports.getUpstreamArgs = getUpstreamArgs;
1184
+ exports.gitAddAll = gitAddAll;
1185
+ exports.gitAddTracked = gitAddTracked;
1186
+ exports.gitCommit = gitCommit;
1187
+ exports.gitCommitAmend = gitCommitAmend;
1188
+ exports.gitTagAnnotated = gitTagAnnotated;
1189
+ exports.gitTagLightweight = gitTagLightweight;
1190
+ exports.gitUndo = gitUndo;
1191
+ exports.hasUpstreamBranch = hasUpstreamBranch;
1192
+ exports.hasWriteAccess = hasWriteAccess;
1193
+ exports.isEmpty = isEmpty;
1194
+ exports.isGitRepo = isGitRepo;
1195
+ exports.isRemoteName = isRemoteName;
1196
+ exports.isTestFile = isTestFile;
1197
+ exports.isValidPackageName = isValidPackageName;
1198
+ exports.isWorkingDirClean = isWorkingDirClean;
1199
+ exports.joinUrl = joinUrl;
1200
+ exports.parseArgs = parseArgs;
1201
+ exports.parseGitHubRepo = parseGitHubRepo;
1202
+ exports.pingRegistry = pingRegistry;
1203
+ exports.pkgFromUserAgent = pkgFromUserAgent;
1204
+ exports.publishPackage = publishPackage;
1205
+ exports.pushBranch = pushBranch;
1206
+ exports.pushTag = pushTag;
1207
+ exports.readJsonFile = readJsonFile;
1208
+ exports.readSubDirs = readSubDirs;
1209
+ exports.registryArg = registryArg;
1210
+ exports.resetHard = resetHard;
1211
+ exports.resetHardSync = resetHardSync;
1212
+ exports.resetMixed = resetMixed;
1213
+ exports.resetSoft = resetSoft;
1214
+ exports.resolveChangelogRange = resolveChangelogRange;
1215
+ exports.restoreAll = restoreAll;
1216
+ exports.restoreAllSync = restoreAllSync;
1217
+ exports.restoreFile = restoreFile;
1218
+ exports.restoreFileFromCommit = restoreFileFromCommit;
1219
+ exports.restoreFromCommit = restoreFromCommit;
1220
+ exports.revertCommit = revertCommit;
1221
+ exports.runCliForTest = runCliForTest;
1222
+ exports.runGit = runGit;
1223
+ exports.runGitSync = runGitSync;
1224
+ exports.runNode = runNode;
1225
+ exports.runNodeSync = runNodeSync;
1226
+ exports.runNpm = runNpm;
1227
+ exports.runNpmSync = runNpmSync;
1228
+ exports.space = space;
1229
+ exports.spawnAsync = spawnAsync;
1230
+ exports.spawnSyncWithString = spawnSyncWithString;
1231
+ exports.stringifyArgs = stringifyArgs;
1232
+ exports.tagArg = tagArg;
1233
+ exports.toValidPackageName = toValidPackageName;
1234
+ exports.toValidProjectName = toValidProjectName;
1235
+ exports.trimTemplate = trimTemplate;
1236
+ exports.unstageAll = unstageAll;
1237
+ exports.unstageFile = unstageFile;