@peiyanlu/cli-utils 0.0.2 → 0.0.4

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.
@@ -1,6 +1,7 @@
1
1
  let node_child_process = require("node:child_process");
2
2
  let node_fs = require("node:fs");
3
3
  let node_fs_promises = require("node:fs/promises");
4
+ let node_os = require("node:os");
4
5
  let node_path = require("node:path");
5
6
  let node_util = require("node:util");
6
7
 
@@ -125,31 +126,37 @@ const isTestFile = (name) => {
125
126
  /^vitest([-.])(.*)\.m?(ts|js)$/
126
127
  ].some((reg) => reg.test(name));
127
128
  };
129
+ /** 解析 Github 链接获取 owner 和 repo */
128
130
  const parseGitHubRepo = (url) => {
129
131
  const match = url.trim().match(/github(?:\.com)?[:/](.+?)\/(.+?)(?:[#/?].+?)?(?:\.git)?$/);
130
132
  return match ? match.slice(1, 3) : [];
131
133
  };
134
+ /** 基于 EOL 的可多换行函数 */
135
+ const eol = (n = 1) => node_os.EOL.repeat(n);
132
136
 
133
137
  //#endregion
134
- //#region src/shell.ts
138
+ //#region src/styleText.ts
135
139
  const dim = (text) => (0, node_util.styleText)(["dim"], text);
136
140
  const red = (text) => (0, node_util.styleText)(["red"], text);
137
- const yellow = (text) => (0, node_util.styleText)(["yellow"], text);
141
+
142
+ //#endregion
143
+ //#region src/shell.ts
144
+ /** 异步执行 `spawn` 获取字符串类型的结果 */
138
145
  const spawnAsync = (cmd, args, options) => {
139
- return new Promise((resolve$1) => {
146
+ return new Promise((resolve$2) => {
140
147
  const { trim, error, dryRun, ...others } = options ?? {};
141
- const fullCmd = [cmd, ...args].join(" ");
148
+ const fullCmd = stringifyArgs([cmd, ...args]);
142
149
  if (dryRun) {
143
150
  console.log(`${dim("[dry-run]")} ${fullCmd}`);
144
- return resolve$1(void 0);
151
+ return resolve$2(void 0);
145
152
  }
146
153
  const child = (0, node_child_process.spawn)(cmd, args, { ...others });
147
154
  let stdout = "";
148
- child.stdout?.setEncoding("utf-8");
155
+ child.stdout.setEncoding("utf-8");
149
156
  child.stdout?.on("data", (data) => stdout += trim ? data.trim() : data);
150
157
  let stderr = "";
151
- child.stderr?.setEncoding("utf-8");
152
- child.stderr?.on("data", (data) => stderr += trim ? data.trim() : data);
158
+ child.stderr.setEncoding("utf-8");
159
+ child.stderr.on("data", (data) => stderr += trim ? data.trim() : data);
153
160
  child.on("close", (code) => {
154
161
  if (stderr) {
155
162
  const err = `${red("spawnAsync")} ${dim(fullCmd)} ${stderr}`;
@@ -160,45 +167,116 @@ const spawnAsync = (cmd, args, options) => {
160
167
  case "throw": throw new Error(err);
161
168
  }
162
169
  }
163
- resolve$1(0 === code ? stdout : void 0);
170
+ resolve$2(0 === code ? stdout : void 0);
164
171
  });
165
172
  });
166
173
  };
167
- const execAsync = (cmd, options) => {
168
- return new Promise((resolve$1) => {
174
+ /** 异步执行 `exec` 获取字符串类型的结果 */
175
+ const execAsync = (cmd, argsOrOptions, maybeOptions) => {
176
+ return new Promise((resolve$2) => {
177
+ let command;
178
+ let options;
179
+ if (Array.isArray(argsOrOptions)) {
180
+ command = stringifyArgs([cmd, ...argsOrOptions]);
181
+ options = maybeOptions;
182
+ } else {
183
+ command = cmd;
184
+ options = argsOrOptions;
185
+ }
169
186
  const { trim, dryRun, error, ...others } = options ?? {};
170
187
  if (dryRun) {
171
- console.log(`${dim("[dry-run]")} ${cmd}`);
172
- return resolve$1(void 0);
188
+ console.log(`${dim("[dry-run]")} ${command}`);
189
+ return resolve$2(void 0);
173
190
  }
174
- (0, node_child_process.exec)(cmd, { ...others }, (err, stdout) => {
175
- if (err) {
176
- const msg = `${red("execAsync")} ${dim(cmd)} ${err.message}`;
191
+ (0, node_child_process.exec)(command, { ...others }, (stderr, stdout) => {
192
+ if (stderr) {
193
+ const err = `${red("execAsync")} ${dim(command)} ${stderr.message}`;
177
194
  switch (error) {
178
195
  case "log":
179
- console.error(msg);
196
+ console.error(err);
180
197
  break;
181
- case "throw": throw new Error(msg);
198
+ case "throw": throw new Error(err);
182
199
  }
183
200
  }
184
- resolve$1(err ? void 0 : trim ? stdout.trim() : stdout);
201
+ resolve$2(stderr ? void 0 : trim ? stdout.trim() : stdout);
185
202
  });
186
203
  });
187
204
  };
188
- const runGit = async (args, trim = true) => {
189
- return spawnAsync("git", args, { trim });
205
+ /** 基于 {@link spawnAsync} 实现 */
206
+ const runGit = async (args, options = { trim: true }) => {
207
+ return spawnAsync("git", args, options);
190
208
  };
191
- const runNpm = (args) => {
192
- return execAsync(["npm", ...args].join(" "));
209
+ /** 基于 {@link execAsync} 实现 */
210
+ const runNpm = (args, options = { trim: true }) => {
211
+ return execAsync("npm", args, options);
212
+ };
213
+ /** 基于 {@link spawnSync} 实现 */
214
+ const runGitSync = (args, options) => {
215
+ const { stdout } = (0, node_child_process.spawnSync)("git", args, {
216
+ encoding: "utf-8",
217
+ ...options
218
+ });
219
+ return stdout.toString().trim();
220
+ };
221
+ /** 基于 {@link execSync} 实现 */
222
+ const runNpmSync = (args, options) => {
223
+ return (0, node_child_process.execSync)(stringifyArgs(["npm", ...args]), {
224
+ encoding: "utf-8",
225
+ ...options
226
+ }).toString().trim();
193
227
  };
228
+ /** 将字符串以空格分割为数组 */
229
+ const parseArgs = (args) => args.trim() ? args.trim().split(" ") : [];
230
+ /** 将数组以空格拼接为字符串 */
231
+ const stringifyArgs = (args) => args.length ? args.join(" ") : "";
232
+ /** 支持所有支持 `--version` 命令的脚本查看版本 */
233
+ const checkVersion = async (cmd) => {
234
+ return execAsync(`${cmd} --version`);
235
+ };
236
+
237
+ //#endregion
238
+ //#region src/joinUrl.ts
239
+ function joinUrl(input) {
240
+ const temps = Array.isArray(input) ? input : [...arguments];
241
+ if (temps.length === 0) return "";
242
+ const result = [];
243
+ const parts = [...temps];
244
+ /** 协议正则 */
245
+ const PROTOCOL_RE = /^[^/:]+:\/*$/;
246
+ const FILE_PROTOCOL_RE = /^file:\/\/\//;
247
+ if (PROTOCOL_RE.test(parts[0]) && parts.length > 1) {
248
+ parts[1] = parts[0] + parts[1];
249
+ parts.shift();
250
+ }
251
+ if (FILE_PROTOCOL_RE.test(parts[0])) parts[0] = parts[0].replace(/^([^/:]+):\/*/, "$1:///");
252
+ else parts[0] = parts[0].replace(/^([^/:]+):\/*/, "$1://");
253
+ parts.forEach((part, index) => {
254
+ if (!part) return;
255
+ let segment = part;
256
+ if (index > 0) segment = segment.replace(/^\/+/, "");
257
+ if (index < parts.length - 1) segment = segment.replace(/\/+$/, "");
258
+ else segment = segment.replace(/\/+$/, "/");
259
+ result.push(segment);
260
+ });
261
+ let url = result.join("/");
262
+ url = url.replace(/\/(\?|&|#[^!])/g, "$1");
263
+ const [base, ...queryParts] = url.split("?");
264
+ url = base + (queryParts.length ? "?" + queryParts.join("&") : "");
265
+ return url;
266
+ }
267
+
268
+ //#endregion
269
+ //#region src/git.ts
270
+ /** 判断指定目录是否是 git 仓库 */
194
271
  const isGitRepo = async (dir) => {
195
- return !!await runGit([
272
+ return "true" === await runGit([
196
273
  "-C",
197
- dir ? `./${dir}` : ".",
274
+ (0, node_path.resolve)(process.cwd(), dir || "."),
198
275
  "rev-parse",
199
276
  "--is-inside-work-tree"
200
277
  ]);
201
278
  };
279
+ /** 获取指定的 git 配置 */
202
280
  const getGitConfig = (key, global = true) => {
203
281
  return runGit([
204
282
  "config",
@@ -206,6 +284,7 @@ const getGitConfig = (key, global = true) => {
206
284
  key
207
285
  ]);
208
286
  };
287
+ /** 获取 git 远程地址 */
209
288
  const getGitRemoteUrl = async (remoteName = "origin") => {
210
289
  return runGit([
211
290
  "remote",
@@ -217,6 +296,10 @@ const getGitRemoteUrl = async (remoteName = "origin") => {
217
296
  `remote.${remoteName}.url`
218
297
  ]));
219
298
  };
299
+
300
+ //#endregion
301
+ //#region src/npm.ts
302
+ /** 获取指定包的版本 */
220
303
  const pkgVersion = (pkg) => {
221
304
  return runNpm([
222
305
  "view",
@@ -224,9 +307,6 @@ const pkgVersion = (pkg) => {
224
307
  "version"
225
308
  ]);
226
309
  };
227
- const checkVersion = async (cmd) => {
228
- return execAsync(`${cmd} --version`);
229
- };
230
310
 
231
311
  //#endregion
232
312
  exports.ConfirmResult = ConfirmResult;
@@ -235,10 +315,10 @@ exports.PkgManager = PkgManager;
235
315
  exports.YesOrNo = YesOrNo;
236
316
  exports.checkVersion = checkVersion;
237
317
  exports.copyDirAsync = copyDirAsync;
238
- exports.dim = dim;
239
318
  exports.editFile = editFile;
240
319
  exports.editJsonFile = editJsonFile;
241
320
  exports.emptyDir = emptyDir;
321
+ exports.eol = eol;
242
322
  exports.execAsync = execAsync;
243
323
  exports.getGitConfig = getGitConfig;
244
324
  exports.getGitRemoteUrl = getGitRemoteUrl;
@@ -246,16 +326,19 @@ exports.isEmpty = isEmpty;
246
326
  exports.isGitRepo = isGitRepo;
247
327
  exports.isTestFile = isTestFile;
248
328
  exports.isValidPackageName = isValidPackageName;
329
+ exports.joinUrl = joinUrl;
330
+ exports.parseArgs = parseArgs;
249
331
  exports.parseGitHubRepo = parseGitHubRepo;
250
332
  exports.pkgFromUserAgent = pkgFromUserAgent;
251
333
  exports.pkgVersion = pkgVersion;
252
334
  exports.readJsonFile = readJsonFile;
253
335
  exports.readSubDirs = readSubDirs;
254
- exports.red = red;
255
336
  exports.runCliForTest = runCliForTest;
256
337
  exports.runGit = runGit;
338
+ exports.runGitSync = runGitSync;
257
339
  exports.runNpm = runNpm;
340
+ exports.runNpmSync = runNpmSync;
258
341
  exports.spawnAsync = spawnAsync;
342
+ exports.stringifyArgs = stringifyArgs;
259
343
  exports.toValidPackageName = toValidPackageName;
260
- exports.toValidProjectName = toValidProjectName;
261
- exports.yellow = yellow;
344
+ exports.toValidProjectName = toValidProjectName;
@@ -1,5 +1,6 @@
1
+ import { ExecOptionsWithStringEncoding, SpawnOptionsWithoutStdio } from "node:child_process";
1
2
  import * as child_process0 from "child_process";
2
- import { ExecOptions, SpawnOptions, SpawnSyncOptionsWithStringEncoding } from "child_process";
3
+ import { ExecSyncOptionsWithStringEncoding, SpawnSyncOptionsWithStringEncoding } from "child_process";
3
4
 
4
5
  //#region src/enums.d.ts
5
6
  declare enum PkgManager {
@@ -38,12 +39,15 @@ interface PkgInfo {
38
39
  version: string;
39
40
  }
40
41
  interface ExecResultOptions {
42
+ /** 去掉结果的首尾空格 */
41
43
  trim?: boolean;
44
+ /** 仅打印命令,不实际执行命令 */
42
45
  dryRun?: boolean;
46
+ /** log: 打印错误信息,返回 undefined; throw: 抛出错误; ignore: 返回 undefined */
43
47
  error?: 'log' | 'throw' | 'ignore';
44
48
  }
45
- type SpawnAsyncOptions<T = SpawnOptions> = T & ExecResultOptions;
46
- type ExecAsyncOptions<T = ExecOptions> = T & ExecResultOptions;
49
+ type SpawnAsyncOptions = SpawnOptionsWithoutStdio & ExecResultOptions;
50
+ type ExecAsyncOptions = ExecOptionsWithStringEncoding & ExecResultOptions;
47
51
  //#endregion
48
52
  //#region src/utils.d.ts
49
53
  declare const isValidPackageName: (packageName: string) => boolean;
@@ -62,20 +66,49 @@ declare const pkgFromUserAgent: (userAgent: string | undefined) => PkgInfo | und
62
66
  declare const runCliForTest: (path: string, args: string[], options?: SpawnSyncOptionsWithStringEncoding) => child_process0.SpawnSyncReturns<string>;
63
67
  /** 判断测试文件(夹) */
64
68
  declare const isTestFile: (name: string) => boolean;
69
+ /** 解析 Github 链接获取 owner 和 repo */
65
70
  declare const parseGitHubRepo: (url: string) => string[];
71
+ /** 基于 EOL 的可多换行函数 */
72
+ declare const eol: (n?: number) => string;
66
73
  //#endregion
67
74
  //#region src/shell.d.ts
68
- declare const dim: (text: string) => string;
69
- declare const red: (text: string) => string;
70
- declare const yellow: (text: string) => string;
71
- declare const spawnAsync: <T = SpawnOptions>(cmd: string, args: string[], options?: SpawnAsyncOptions<T>) => Promise<string | undefined>;
72
- declare const execAsync: <T = ExecOptions>(cmd: string, options?: ExecAsyncOptions<T>) => Promise<string | undefined>;
73
- declare const runGit: (args: string[], trim?: boolean) => Promise<string | undefined>;
74
- declare const runNpm: (args: string[]) => Promise<string | undefined>;
75
+ /** 异步执行 `spawn` 获取字符串类型的结果 */
76
+ declare const spawnAsync: (cmd: string, args: string[], options?: SpawnAsyncOptions) => Promise<string | undefined>;
77
+ type ExecAsync = {
78
+ (cmd: string, options?: ExecAsyncOptions): Promise<string | undefined>;
79
+ (cmd: string, args: string[], options?: ExecAsyncOptions): Promise<string | undefined>;
80
+ };
81
+ /** 异步执行 `exec` 获取字符串类型的结果 */
82
+ declare const execAsync: ExecAsync;
83
+ /** 基于 {@link spawnAsync} 实现 */
84
+ declare const runGit: (args: string[], options?: SpawnAsyncOptions) => Promise<string | undefined>;
85
+ /** 基于 {@link execAsync} 实现 */
86
+ declare const runNpm: (args: string[], options?: ExecAsyncOptions) => Promise<string | undefined>;
87
+ /** 基于 {@link spawnSync} 实现 */
88
+ declare const runGitSync: (args: string[], options?: SpawnSyncOptionsWithStringEncoding) => string;
89
+ /** 基于 {@link execSync} 实现 */
90
+ declare const runNpmSync: (args: string[], options?: ExecSyncOptionsWithStringEncoding) => string;
91
+ /** 将字符串以空格分割为数组 */
92
+ declare const parseArgs: (args: string) => string[];
93
+ /** 将数组以空格拼接为字符串 */
94
+ declare const stringifyArgs: (args: string[]) => string;
95
+ /** 支持所有支持 `--version` 命令的脚本查看版本 */
96
+ declare const checkVersion: (cmd: string) => Promise<string | undefined>;
97
+ //#endregion
98
+ //#region src/joinUrl.d.ts
99
+ declare function joinUrl(...args: string[]): string;
100
+ declare function joinUrl(input: readonly string[]): string;
101
+ //#endregion
102
+ //#region src/git.d.ts
103
+ /** 判断指定目录是否是 git 仓库 */
75
104
  declare const isGitRepo: (dir?: string) => Promise<boolean>;
105
+ /** 获取指定的 git 配置 */
76
106
  declare const getGitConfig: (key: string, global?: boolean) => Promise<string | undefined>;
107
+ /** 获取 git 远程地址 */
77
108
  declare const getGitRemoteUrl: (remoteName?: string) => Promise<string | undefined>;
109
+ //#endregion
110
+ //#region src/npm.d.ts
111
+ /** 获取指定包的版本 */
78
112
  declare const pkgVersion: (pkg: string) => Promise<string | undefined>;
79
- declare const checkVersion: (cmd: string) => Promise<string | undefined>;
80
113
  //#endregion
81
- export { CliOptions, ConfirmResult, CopyOptions, ExecAsyncOptions, ExecResultOptions, HttpLibrary, PkgInfo, PkgManager, SpawnAsyncOptions, YesOrNo, checkVersion, copyDirAsync, dim, editFile, editJsonFile, emptyDir, execAsync, getGitConfig, getGitRemoteUrl, isEmpty, isGitRepo, isTestFile, isValidPackageName, parseGitHubRepo, pkgFromUserAgent, pkgVersion, readJsonFile, readSubDirs, red, runCliForTest, runGit, runNpm, spawnAsync, toValidPackageName, toValidProjectName, yellow };
114
+ export { CliOptions, ConfirmResult, CopyOptions, ExecAsyncOptions, ExecResultOptions, HttpLibrary, PkgInfo, PkgManager, SpawnAsyncOptions, YesOrNo, checkVersion, copyDirAsync, editFile, editJsonFile, emptyDir, eol, execAsync, getGitConfig, getGitRemoteUrl, isEmpty, isGitRepo, isTestFile, isValidPackageName, joinUrl, parseArgs, parseGitHubRepo, pkgFromUserAgent, pkgVersion, readJsonFile, readSubDirs, runCliForTest, runGit, runGitSync, runNpm, runNpmSync, spawnAsync, stringifyArgs, toValidPackageName, toValidProjectName };
@@ -1,5 +1,6 @@
1
+ import { ExecOptionsWithStringEncoding, SpawnOptionsWithoutStdio } from "node:child_process";
1
2
  import * as child_process0 from "child_process";
2
- import { ExecOptions, SpawnOptions, SpawnSyncOptionsWithStringEncoding } from "child_process";
3
+ import { ExecSyncOptionsWithStringEncoding, SpawnSyncOptionsWithStringEncoding } from "child_process";
3
4
 
4
5
  //#region src/enums.d.ts
5
6
  declare enum PkgManager {
@@ -38,12 +39,15 @@ interface PkgInfo {
38
39
  version: string;
39
40
  }
40
41
  interface ExecResultOptions {
42
+ /** 去掉结果的首尾空格 */
41
43
  trim?: boolean;
44
+ /** 仅打印命令,不实际执行命令 */
42
45
  dryRun?: boolean;
46
+ /** log: 打印错误信息,返回 undefined; throw: 抛出错误; ignore: 返回 undefined */
43
47
  error?: 'log' | 'throw' | 'ignore';
44
48
  }
45
- type SpawnAsyncOptions<T = SpawnOptions> = T & ExecResultOptions;
46
- type ExecAsyncOptions<T = ExecOptions> = T & ExecResultOptions;
49
+ type SpawnAsyncOptions = SpawnOptionsWithoutStdio & ExecResultOptions;
50
+ type ExecAsyncOptions = ExecOptionsWithStringEncoding & ExecResultOptions;
47
51
  //#endregion
48
52
  //#region src/utils.d.ts
49
53
  declare const isValidPackageName: (packageName: string) => boolean;
@@ -62,20 +66,49 @@ declare const pkgFromUserAgent: (userAgent: string | undefined) => PkgInfo | und
62
66
  declare const runCliForTest: (path: string, args: string[], options?: SpawnSyncOptionsWithStringEncoding) => child_process0.SpawnSyncReturns<string>;
63
67
  /** 判断测试文件(夹) */
64
68
  declare const isTestFile: (name: string) => boolean;
69
+ /** 解析 Github 链接获取 owner 和 repo */
65
70
  declare const parseGitHubRepo: (url: string) => string[];
71
+ /** 基于 EOL 的可多换行函数 */
72
+ declare const eol: (n?: number) => string;
66
73
  //#endregion
67
74
  //#region src/shell.d.ts
68
- declare const dim: (text: string) => string;
69
- declare const red: (text: string) => string;
70
- declare const yellow: (text: string) => string;
71
- declare const spawnAsync: <T = SpawnOptions>(cmd: string, args: string[], options?: SpawnAsyncOptions<T>) => Promise<string | undefined>;
72
- declare const execAsync: <T = ExecOptions>(cmd: string, options?: ExecAsyncOptions<T>) => Promise<string | undefined>;
73
- declare const runGit: (args: string[], trim?: boolean) => Promise<string | undefined>;
74
- declare const runNpm: (args: string[]) => Promise<string | undefined>;
75
+ /** 异步执行 `spawn` 获取字符串类型的结果 */
76
+ declare const spawnAsync: (cmd: string, args: string[], options?: SpawnAsyncOptions) => Promise<string | undefined>;
77
+ type ExecAsync = {
78
+ (cmd: string, options?: ExecAsyncOptions): Promise<string | undefined>;
79
+ (cmd: string, args: string[], options?: ExecAsyncOptions): Promise<string | undefined>;
80
+ };
81
+ /** 异步执行 `exec` 获取字符串类型的结果 */
82
+ declare const execAsync: ExecAsync;
83
+ /** 基于 {@link spawnAsync} 实现 */
84
+ declare const runGit: (args: string[], options?: SpawnAsyncOptions) => Promise<string | undefined>;
85
+ /** 基于 {@link execAsync} 实现 */
86
+ declare const runNpm: (args: string[], options?: ExecAsyncOptions) => Promise<string | undefined>;
87
+ /** 基于 {@link spawnSync} 实现 */
88
+ declare const runGitSync: (args: string[], options?: SpawnSyncOptionsWithStringEncoding) => string;
89
+ /** 基于 {@link execSync} 实现 */
90
+ declare const runNpmSync: (args: string[], options?: ExecSyncOptionsWithStringEncoding) => string;
91
+ /** 将字符串以空格分割为数组 */
92
+ declare const parseArgs: (args: string) => string[];
93
+ /** 将数组以空格拼接为字符串 */
94
+ declare const stringifyArgs: (args: string[]) => string;
95
+ /** 支持所有支持 `--version` 命令的脚本查看版本 */
96
+ declare const checkVersion: (cmd: string) => Promise<string | undefined>;
97
+ //#endregion
98
+ //#region src/joinUrl.d.ts
99
+ declare function joinUrl(...args: string[]): string;
100
+ declare function joinUrl(input: readonly string[]): string;
101
+ //#endregion
102
+ //#region src/git.d.ts
103
+ /** 判断指定目录是否是 git 仓库 */
75
104
  declare const isGitRepo: (dir?: string) => Promise<boolean>;
105
+ /** 获取指定的 git 配置 */
76
106
  declare const getGitConfig: (key: string, global?: boolean) => Promise<string | undefined>;
107
+ /** 获取 git 远程地址 */
77
108
  declare const getGitRemoteUrl: (remoteName?: string) => Promise<string | undefined>;
109
+ //#endregion
110
+ //#region src/npm.d.ts
111
+ /** 获取指定包的版本 */
78
112
  declare const pkgVersion: (pkg: string) => Promise<string | undefined>;
79
- declare const checkVersion: (cmd: string) => Promise<string | undefined>;
80
113
  //#endregion
81
- export { CliOptions, ConfirmResult, CopyOptions, ExecAsyncOptions, ExecResultOptions, HttpLibrary, PkgInfo, PkgManager, SpawnAsyncOptions, YesOrNo, checkVersion, copyDirAsync, dim, editFile, editJsonFile, emptyDir, execAsync, getGitConfig, getGitRemoteUrl, isEmpty, isGitRepo, isTestFile, isValidPackageName, parseGitHubRepo, pkgFromUserAgent, pkgVersion, readJsonFile, readSubDirs, red, runCliForTest, runGit, runNpm, spawnAsync, toValidPackageName, toValidProjectName, yellow };
114
+ export { CliOptions, ConfirmResult, CopyOptions, ExecAsyncOptions, ExecResultOptions, HttpLibrary, PkgInfo, PkgManager, SpawnAsyncOptions, YesOrNo, checkVersion, copyDirAsync, editFile, editJsonFile, emptyDir, eol, execAsync, getGitConfig, getGitRemoteUrl, isEmpty, isGitRepo, isTestFile, isValidPackageName, joinUrl, parseArgs, parseGitHubRepo, pkgFromUserAgent, pkgVersion, readJsonFile, readSubDirs, runCliForTest, runGit, runGitSync, runNpm, runNpmSync, spawnAsync, stringifyArgs, toValidPackageName, toValidProjectName };
@@ -1,6 +1,7 @@
1
- import { exec, spawn, spawnSync } from "node:child_process";
1
+ import { exec, execSync, spawn, spawnSync } from "node:child_process";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { copyFile, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
4
+ import { EOL } from "node:os";
4
5
  import { join, resolve } from "node:path";
5
6
  import { styleText } from "node:util";
6
7
 
@@ -125,31 +126,37 @@ const isTestFile = (name) => {
125
126
  /^vitest([-.])(.*)\.m?(ts|js)$/
126
127
  ].some((reg) => reg.test(name));
127
128
  };
129
+ /** 解析 Github 链接获取 owner 和 repo */
128
130
  const parseGitHubRepo = (url) => {
129
131
  const match = url.trim().match(/github(?:\.com)?[:/](.+?)\/(.+?)(?:[#/?].+?)?(?:\.git)?$/);
130
132
  return match ? match.slice(1, 3) : [];
131
133
  };
134
+ /** 基于 EOL 的可多换行函数 */
135
+ const eol = (n = 1) => EOL.repeat(n);
132
136
 
133
137
  //#endregion
134
- //#region src/shell.ts
138
+ //#region src/styleText.ts
135
139
  const dim = (text) => styleText(["dim"], text);
136
140
  const red = (text) => styleText(["red"], text);
137
- const yellow = (text) => styleText(["yellow"], text);
141
+
142
+ //#endregion
143
+ //#region src/shell.ts
144
+ /** 异步执行 `spawn` 获取字符串类型的结果 */
138
145
  const spawnAsync = (cmd, args, options) => {
139
146
  return new Promise((resolve$1) => {
140
147
  const { trim, error, dryRun, ...others } = options ?? {};
141
- const fullCmd = [cmd, ...args].join(" ");
148
+ const fullCmd = stringifyArgs([cmd, ...args]);
142
149
  if (dryRun) {
143
150
  console.log(`${dim("[dry-run]")} ${fullCmd}`);
144
151
  return resolve$1(void 0);
145
152
  }
146
153
  const child = spawn(cmd, args, { ...others });
147
154
  let stdout = "";
148
- child.stdout?.setEncoding("utf-8");
155
+ child.stdout.setEncoding("utf-8");
149
156
  child.stdout?.on("data", (data) => stdout += trim ? data.trim() : data);
150
157
  let stderr = "";
151
- child.stderr?.setEncoding("utf-8");
152
- child.stderr?.on("data", (data) => stderr += trim ? data.trim() : data);
158
+ child.stderr.setEncoding("utf-8");
159
+ child.stderr.on("data", (data) => stderr += trim ? data.trim() : data);
153
160
  child.on("close", (code) => {
154
161
  if (stderr) {
155
162
  const err = `${red("spawnAsync")} ${dim(fullCmd)} ${stderr}`;
@@ -164,41 +171,112 @@ const spawnAsync = (cmd, args, options) => {
164
171
  });
165
172
  });
166
173
  };
167
- const execAsync = (cmd, options) => {
174
+ /** 异步执行 `exec` 获取字符串类型的结果 */
175
+ const execAsync = (cmd, argsOrOptions, maybeOptions) => {
168
176
  return new Promise((resolve$1) => {
177
+ let command;
178
+ let options;
179
+ if (Array.isArray(argsOrOptions)) {
180
+ command = stringifyArgs([cmd, ...argsOrOptions]);
181
+ options = maybeOptions;
182
+ } else {
183
+ command = cmd;
184
+ options = argsOrOptions;
185
+ }
169
186
  const { trim, dryRun, error, ...others } = options ?? {};
170
187
  if (dryRun) {
171
- console.log(`${dim("[dry-run]")} ${cmd}`);
188
+ console.log(`${dim("[dry-run]")} ${command}`);
172
189
  return resolve$1(void 0);
173
190
  }
174
- exec(cmd, { ...others }, (err, stdout) => {
175
- if (err) {
176
- const msg = `${red("execAsync")} ${dim(cmd)} ${err.message}`;
191
+ exec(command, { ...others }, (stderr, stdout) => {
192
+ if (stderr) {
193
+ const err = `${red("execAsync")} ${dim(command)} ${stderr.message}`;
177
194
  switch (error) {
178
195
  case "log":
179
- console.error(msg);
196
+ console.error(err);
180
197
  break;
181
- case "throw": throw new Error(msg);
198
+ case "throw": throw new Error(err);
182
199
  }
183
200
  }
184
- resolve$1(err ? void 0 : trim ? stdout.trim() : stdout);
201
+ resolve$1(stderr ? void 0 : trim ? stdout.trim() : stdout);
185
202
  });
186
203
  });
187
204
  };
188
- const runGit = async (args, trim = true) => {
189
- return spawnAsync("git", args, { trim });
205
+ /** 基于 {@link spawnAsync} 实现 */
206
+ const runGit = async (args, options = { trim: true }) => {
207
+ return spawnAsync("git", args, options);
190
208
  };
191
- const runNpm = (args) => {
192
- return execAsync(["npm", ...args].join(" "));
209
+ /** 基于 {@link execAsync} 实现 */
210
+ const runNpm = (args, options = { trim: true }) => {
211
+ return execAsync("npm", args, options);
212
+ };
213
+ /** 基于 {@link spawnSync} 实现 */
214
+ const runGitSync = (args, options) => {
215
+ const { stdout } = spawnSync("git", args, {
216
+ encoding: "utf-8",
217
+ ...options
218
+ });
219
+ return stdout.toString().trim();
220
+ };
221
+ /** 基于 {@link execSync} 实现 */
222
+ const runNpmSync = (args, options) => {
223
+ return execSync(stringifyArgs(["npm", ...args]), {
224
+ encoding: "utf-8",
225
+ ...options
226
+ }).toString().trim();
193
227
  };
228
+ /** 将字符串以空格分割为数组 */
229
+ const parseArgs = (args) => args.trim() ? args.trim().split(" ") : [];
230
+ /** 将数组以空格拼接为字符串 */
231
+ const stringifyArgs = (args) => args.length ? args.join(" ") : "";
232
+ /** 支持所有支持 `--version` 命令的脚本查看版本 */
233
+ const checkVersion = async (cmd) => {
234
+ return execAsync(`${cmd} --version`);
235
+ };
236
+
237
+ //#endregion
238
+ //#region src/joinUrl.ts
239
+ function joinUrl(input) {
240
+ const temps = Array.isArray(input) ? input : [...arguments];
241
+ if (temps.length === 0) return "";
242
+ const result = [];
243
+ const parts = [...temps];
244
+ /** 协议正则 */
245
+ const PROTOCOL_RE = /^[^/:]+:\/*$/;
246
+ const FILE_PROTOCOL_RE = /^file:\/\/\//;
247
+ if (PROTOCOL_RE.test(parts[0]) && parts.length > 1) {
248
+ parts[1] = parts[0] + parts[1];
249
+ parts.shift();
250
+ }
251
+ if (FILE_PROTOCOL_RE.test(parts[0])) parts[0] = parts[0].replace(/^([^/:]+):\/*/, "$1:///");
252
+ else parts[0] = parts[0].replace(/^([^/:]+):\/*/, "$1://");
253
+ parts.forEach((part, index) => {
254
+ if (!part) return;
255
+ let segment = part;
256
+ if (index > 0) segment = segment.replace(/^\/+/, "");
257
+ if (index < parts.length - 1) segment = segment.replace(/\/+$/, "");
258
+ else segment = segment.replace(/\/+$/, "/");
259
+ result.push(segment);
260
+ });
261
+ let url = result.join("/");
262
+ url = url.replace(/\/(\?|&|#[^!])/g, "$1");
263
+ const [base, ...queryParts] = url.split("?");
264
+ url = base + (queryParts.length ? "?" + queryParts.join("&") : "");
265
+ return url;
266
+ }
267
+
268
+ //#endregion
269
+ //#region src/git.ts
270
+ /** 判断指定目录是否是 git 仓库 */
194
271
  const isGitRepo = async (dir) => {
195
- return !!await runGit([
272
+ return "true" === await runGit([
196
273
  "-C",
197
- dir ? `./${dir}` : ".",
274
+ resolve(process.cwd(), dir || "."),
198
275
  "rev-parse",
199
276
  "--is-inside-work-tree"
200
277
  ]);
201
278
  };
279
+ /** 获取指定的 git 配置 */
202
280
  const getGitConfig = (key, global = true) => {
203
281
  return runGit([
204
282
  "config",
@@ -206,6 +284,7 @@ const getGitConfig = (key, global = true) => {
206
284
  key
207
285
  ]);
208
286
  };
287
+ /** 获取 git 远程地址 */
209
288
  const getGitRemoteUrl = async (remoteName = "origin") => {
210
289
  return runGit([
211
290
  "remote",
@@ -217,6 +296,10 @@ const getGitRemoteUrl = async (remoteName = "origin") => {
217
296
  `remote.${remoteName}.url`
218
297
  ]));
219
298
  };
299
+
300
+ //#endregion
301
+ //#region src/npm.ts
302
+ /** 获取指定包的版本 */
220
303
  const pkgVersion = (pkg) => {
221
304
  return runNpm([
222
305
  "view",
@@ -224,9 +307,6 @@ const pkgVersion = (pkg) => {
224
307
  "version"
225
308
  ]);
226
309
  };
227
- const checkVersion = async (cmd) => {
228
- return execAsync(`${cmd} --version`);
229
- };
230
310
 
231
311
  //#endregion
232
- export { ConfirmResult, HttpLibrary, PkgManager, YesOrNo, checkVersion, copyDirAsync, dim, editFile, editJsonFile, emptyDir, execAsync, getGitConfig, getGitRemoteUrl, isEmpty, isGitRepo, isTestFile, isValidPackageName, parseGitHubRepo, pkgFromUserAgent, pkgVersion, readJsonFile, readSubDirs, red, runCliForTest, runGit, runNpm, spawnAsync, toValidPackageName, toValidProjectName, yellow };
312
+ export { ConfirmResult, HttpLibrary, PkgManager, YesOrNo, checkVersion, copyDirAsync, editFile, editJsonFile, emptyDir, eol, execAsync, getGitConfig, getGitRemoteUrl, isEmpty, isGitRepo, isTestFile, isValidPackageName, joinUrl, parseArgs, parseGitHubRepo, pkgFromUserAgent, pkgVersion, readJsonFile, readSubDirs, runCliForTest, runGit, runGitSync, runNpm, runNpmSync, spawnAsync, stringifyArgs, toValidPackageName, toValidProjectName };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peiyanlu/cli-utils",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Shared utils for building interactive Node.js CLI applications.",
5
5
  "license": "MIT",
6
6
  "type": "module",