@peiyanlu/cli-utils 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @peiyanlu/cli-utils
2
2
 
3
- My project description.
3
+ Shared utils for building interactive Node.js CLI applications.
4
4
 
5
5
  ## Project setup
6
6
 
@@ -21,12 +21,9 @@ $ pnpm run build
21
21
  ## Run tests
22
22
 
23
23
  ```bash
24
- # unit tests
24
+ # tests
25
25
  $ pnpm run test
26
26
 
27
- # e2e tests
28
- $ pnpm run test:e2e
29
-
30
27
  # test coverage
31
28
  $ pnpm run test:cov
32
29
  ```
@@ -1,7 +1,9 @@
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");
6
+ let node_util = require("node:util");
5
7
 
6
8
  //#region src/enums.ts
7
9
  let PkgManager = /* @__PURE__ */ function(PkgManager$1) {
@@ -35,17 +37,6 @@ let HttpLibrary = /* @__PURE__ */ function(HttpLibrary$1) {
35
37
 
36
38
  //#endregion
37
39
  //#region src/utils.ts
38
- const execAsync = (cmd) => {
39
- return new Promise((r) => {
40
- (0, node_child_process.exec)(cmd, (err, stdout) => r(err ? void 0 : stdout.trim()));
41
- });
42
- };
43
- const pkgVersion = (pkg) => {
44
- return execAsync(`npm view ${pkg} version`);
45
- };
46
- const checkVersion = async (cmd) => {
47
- return execAsync(`${cmd} --version`);
48
- };
49
40
  const isValidPackageName = (packageName) => {
50
41
  return /^(?:@[a-z\d\-*~][a-z\d\-*._~]*\/)?[a-z\d\-~][a-z\d\-._~]*$/.test(packageName);
51
42
  };
@@ -81,15 +72,9 @@ const editJsonFile = (file, callback) => {
81
72
  }
82
73
  });
83
74
  };
84
- const isGitRepo = async (dir) => {
85
- return !!await execAsync(`git -C "${dir ? `./${dir}` : "."}" rev-parse --is-inside-work-tree`);
86
- };
87
75
  const readSubDirs = async (source, ignore = []) => {
88
76
  return (await (0, node_fs_promises.readdir)(source, { withFileTypes: true })).filter((k) => k.isDirectory() && !ignore.includes(k.name)).map((dir) => dir.name);
89
77
  };
90
- const getGitConfig = (key, global = true) => {
91
- return execAsync(`git config ${global ? "--global" : ""} ${key}`);
92
- };
93
78
  const copyDirAsync = async (src, dest, options) => {
94
79
  await (0, node_fs_promises.mkdir)(dest, { recursive: true });
95
80
  const entries = await (0, node_fs_promises.readdir)(src, { withFileTypes: true });
@@ -113,13 +98,7 @@ const readJsonFile = (file) => {
113
98
  return {};
114
99
  }
115
100
  };
116
- /**
117
- * 通过包管理器执行脚本时生效
118
- *
119
- * UserAgent: `process.env.npm_config_user_agent`
120
- * @param {string | undefined} userAgent
121
- * @returns {PkgInfo | undefined}
122
- */
101
+ /** 通过包管理器执行脚本时生效 UserAgent: `process.env.npm_config_user_agent` */
123
102
  const pkgFromUserAgent = (userAgent) => {
124
103
  if (!userAgent) return void 0;
125
104
  const [name, version] = userAgent.split(" ")[0].split("/");
@@ -128,7 +107,8 @@ const pkgFromUserAgent = (userAgent) => {
128
107
  version
129
108
  };
130
109
  };
131
- const runCli = (path, args, options) => {
110
+ /** 同步执行 Node CLI(用于测试环境) */
111
+ const runCliForTest = (path, args, options) => {
132
112
  return (0, node_child_process.spawnSync)("node", [path, ...args], {
133
113
  env: {
134
114
  ...process.env,
@@ -138,6 +118,158 @@ const runCli = (path, args, options) => {
138
118
  ...options
139
119
  });
140
120
  };
121
+ /** 判断测试文件(夹) */
122
+ const isTestFile = (name) => {
123
+ return [
124
+ /(^|[\\/])(test(s?)|__test(s?)__)([\\/]|$)/,
125
+ /\.([a-zA-Z0-9]+-)?(test|spec)\.m?(ts|js)$/,
126
+ /^vitest([-.])(.*)\.m?(ts|js)$/
127
+ ].some((reg) => reg.test(name));
128
+ };
129
+ /** 解析 Github 链接获取 owner 和 repo */
130
+ const parseGitHubRepo = (url) => {
131
+ const match = url.trim().match(/github(?:\.com)?[:/](.+?)\/(.+?)(?:[#/?].+?)?(?:\.git)?$/);
132
+ return match ? match.slice(1, 3) : [];
133
+ };
134
+ /** 基于 EOL 的可多换行函数 */
135
+ const eol = (n = 1) => node_os.EOL.repeat(n);
136
+
137
+ //#endregion
138
+ //#region src/shell.ts
139
+ const dim = (text) => (0, node_util.styleText)(["dim"], text);
140
+ const red = (text) => (0, node_util.styleText)(["red"], text);
141
+ const yellow = (text) => (0, node_util.styleText)(["yellow"], text);
142
+ const underline = (text) => (0, node_util.styleText)(["underline"], text);
143
+ const spawnAsync = (cmd, args, options) => {
144
+ return new Promise((resolve$1) => {
145
+ const { trim, error, dryRun, ...others } = options ?? {};
146
+ const fullCmd = [cmd, ...args].join(" ");
147
+ if (dryRun) {
148
+ console.log(`${dim("[dry-run]")} ${fullCmd}`);
149
+ return resolve$1(void 0);
150
+ }
151
+ const child = (0, node_child_process.spawn)(cmd, args, { ...others });
152
+ let stdout = "";
153
+ child.stdout?.setEncoding("utf-8");
154
+ child.stdout?.on("data", (data) => stdout += trim ? data.trim() : data);
155
+ let stderr = "";
156
+ child.stderr?.setEncoding("utf-8");
157
+ child.stderr?.on("data", (data) => stderr += trim ? data.trim() : data);
158
+ child.on("close", (code) => {
159
+ if (stderr) {
160
+ const err = `${red("spawnAsync")} ${dim(fullCmd)} ${stderr}`;
161
+ switch (error) {
162
+ case "log":
163
+ console.error(err);
164
+ break;
165
+ case "throw": throw new Error(err);
166
+ }
167
+ }
168
+ resolve$1(0 === code ? stdout : void 0);
169
+ });
170
+ });
171
+ };
172
+ const execAsync = (cmd, options) => {
173
+ return new Promise((resolve$1) => {
174
+ const { trim, dryRun, error, ...others } = options ?? {};
175
+ if (dryRun) {
176
+ console.log(`${dim("[dry-run]")} ${cmd}`);
177
+ return resolve$1(void 0);
178
+ }
179
+ (0, node_child_process.exec)(cmd, { ...others }, (err, stdout) => {
180
+ if (err) {
181
+ const msg = `${red("execAsync")} ${dim(cmd)} ${err.message}`;
182
+ switch (error) {
183
+ case "log":
184
+ console.error(msg);
185
+ break;
186
+ case "throw": throw new Error(msg);
187
+ }
188
+ }
189
+ resolve$1(err ? void 0 : trim ? stdout.trim() : stdout);
190
+ });
191
+ });
192
+ };
193
+ const runGit = async (args, options = { trim: true }) => {
194
+ return spawnAsync("git", args, options);
195
+ };
196
+ const runNpm = (args, options = { trim: true }) => {
197
+ return execAsync(["npm", ...args].join(" "), options);
198
+ };
199
+ const fixArgs = (args) => args.trim() ? args.trim().split(" ") : [];
200
+ const checkVersion = async (cmd) => {
201
+ return execAsync(`${cmd} --version`);
202
+ };
203
+
204
+ //#endregion
205
+ //#region src/joinUrl.ts
206
+ function joinUrl(input) {
207
+ const temps = Array.isArray(input) ? input : [...arguments];
208
+ if (temps.length === 0) return "";
209
+ const result = [];
210
+ const parts = [...temps];
211
+ /** 协议正则 */
212
+ const PROTOCOL_RE = /^[^/:]+:\/*$/;
213
+ const FILE_PROTOCOL_RE = /^file:\/\/\//;
214
+ if (PROTOCOL_RE.test(parts[0]) && parts.length > 1) {
215
+ parts[1] = parts[0] + parts[1];
216
+ parts.shift();
217
+ }
218
+ if (FILE_PROTOCOL_RE.test(parts[0])) parts[0] = parts[0].replace(/^([^/:]+):\/*/, "$1:///");
219
+ else parts[0] = parts[0].replace(/^([^/:]+):\/*/, "$1://");
220
+ parts.forEach((part, index) => {
221
+ if (!part) return;
222
+ let segment = part;
223
+ if (index > 0) segment = segment.replace(/^\/+/, "");
224
+ if (index < parts.length - 1) segment = segment.replace(/\/+$/, "");
225
+ else segment = segment.replace(/\/+$/, "/");
226
+ result.push(segment);
227
+ });
228
+ let url = result.join("/");
229
+ url = url.replace(/\/(\?|&|#[^!])/g, "$1");
230
+ const [base, ...queryParts] = url.split("?");
231
+ url = base + (queryParts.length ? "?" + queryParts.join("&") : "");
232
+ return url;
233
+ }
234
+
235
+ //#endregion
236
+ //#region src/git.ts
237
+ const isGitRepo = async (dir) => {
238
+ return !!await runGit([
239
+ "-C",
240
+ dir ? `./${dir}` : ".",
241
+ "rev-parse",
242
+ "--is-inside-work-tree"
243
+ ]);
244
+ };
245
+ const getGitConfig = (key, global = true) => {
246
+ return runGit([
247
+ "config",
248
+ ...global ? ["--global"] : [],
249
+ key
250
+ ]);
251
+ };
252
+ const getGitRemoteUrl = async (remoteName = "origin") => {
253
+ return runGit([
254
+ "remote",
255
+ "get-url",
256
+ remoteName
257
+ ]).catch((_) => runGit([
258
+ "config",
259
+ "--get",
260
+ `remote.${remoteName}.url`
261
+ ]));
262
+ };
263
+
264
+ //#endregion
265
+ //#region src/npm.ts
266
+ const pkgVersion = (pkg) => {
267
+ return runNpm([
268
+ "view",
269
+ pkg,
270
+ "version"
271
+ ]);
272
+ };
141
273
 
142
274
  //#endregion
143
275
  exports.ConfirmResult = ConfirmResult;
@@ -146,18 +278,31 @@ exports.PkgManager = PkgManager;
146
278
  exports.YesOrNo = YesOrNo;
147
279
  exports.checkVersion = checkVersion;
148
280
  exports.copyDirAsync = copyDirAsync;
281
+ exports.dim = dim;
149
282
  exports.editFile = editFile;
150
283
  exports.editJsonFile = editJsonFile;
151
284
  exports.emptyDir = emptyDir;
285
+ exports.eol = eol;
152
286
  exports.execAsync = execAsync;
287
+ exports.fixArgs = fixArgs;
153
288
  exports.getGitConfig = getGitConfig;
289
+ exports.getGitRemoteUrl = getGitRemoteUrl;
154
290
  exports.isEmpty = isEmpty;
155
291
  exports.isGitRepo = isGitRepo;
292
+ exports.isTestFile = isTestFile;
156
293
  exports.isValidPackageName = isValidPackageName;
294
+ exports.joinUrl = joinUrl;
295
+ exports.parseGitHubRepo = parseGitHubRepo;
157
296
  exports.pkgFromUserAgent = pkgFromUserAgent;
158
297
  exports.pkgVersion = pkgVersion;
159
298
  exports.readJsonFile = readJsonFile;
160
299
  exports.readSubDirs = readSubDirs;
161
- exports.runCli = runCli;
300
+ exports.red = red;
301
+ exports.runCliForTest = runCliForTest;
302
+ exports.runGit = runGit;
303
+ exports.runNpm = runNpm;
304
+ exports.spawnAsync = spawnAsync;
162
305
  exports.toValidPackageName = toValidPackageName;
163
- exports.toValidProjectName = toValidProjectName;
306
+ exports.toValidProjectName = toValidProjectName;
307
+ exports.underline = underline;
308
+ exports.yellow = yellow;
@@ -1,5 +1,5 @@
1
1
  import * as child_process0 from "child_process";
2
- import { SpawnSyncOptionsWithStringEncoding } from "child_process";
2
+ import { ExecOptions, SpawnOptions, SpawnSyncOptionsWithStringEncoding } from "child_process";
3
3
 
4
4
  //#region src/enums.d.ts
5
5
  declare enum PkgManager {
@@ -37,11 +37,15 @@ interface PkgInfo {
37
37
  name: string;
38
38
  version: string;
39
39
  }
40
+ interface ExecResultOptions {
41
+ trim?: boolean;
42
+ dryRun?: boolean;
43
+ error?: 'log' | 'throw' | 'ignore';
44
+ }
45
+ type SpawnAsyncOptions<T = SpawnOptions> = T & ExecResultOptions;
46
+ type ExecAsyncOptions<T = ExecOptions> = T & ExecResultOptions;
40
47
  //#endregion
41
48
  //#region src/utils.d.ts
42
- declare const execAsync: (cmd: string) => Promise<string | undefined>;
43
- declare const pkgVersion: (pkg: string) => Promise<string | undefined>;
44
- declare const checkVersion: (cmd: string) => Promise<string | undefined>;
45
49
  declare const isValidPackageName: (packageName: string) => boolean;
46
50
  declare const toValidPackageName: (packageName: string) => string;
47
51
  declare const toValidProjectName: (projectName: string) => string;
@@ -49,19 +53,42 @@ declare const emptyDir: (dir: string, ignore?: string[]) => Promise<boolean>;
49
53
  declare const isEmpty: (path: string, ignore?: string[]) => Promise<boolean>;
50
54
  declare const editFile: (file: string, callback: (content: string) => string) => Promise<void>;
51
55
  declare const editJsonFile: <T extends Record<string, any>>(file: string, callback: (json: T) => void) => Promise<void>;
52
- declare const isGitRepo: (dir?: string) => Promise<boolean>;
53
56
  declare const readSubDirs: (source: string, ignore?: string[]) => Promise<string[]>;
54
- declare const getGitConfig: (key: string, global?: boolean) => Promise<string | undefined>;
55
57
  declare const copyDirAsync: (src: string, dest: string, options: CopyOptions) => Promise<void>;
56
58
  declare const readJsonFile: <T extends Record<string, any>>(file: string) => T;
57
- /**
58
- * 通过包管理器执行脚本时生效
59
- *
60
- * UserAgent: `process.env.npm_config_user_agent`
61
- * @param {string | undefined} userAgent
62
- * @returns {PkgInfo | undefined}
63
- */
59
+ /** 通过包管理器执行脚本时生效 UserAgent: `process.env.npm_config_user_agent` */
64
60
  declare const pkgFromUserAgent: (userAgent: string | undefined) => PkgInfo | undefined;
65
- declare const runCli: (path: string, args: string[], options?: SpawnSyncOptionsWithStringEncoding) => child_process0.SpawnSyncReturns<string>;
61
+ /** 同步执行 Node CLI(用于测试环境) */
62
+ declare const runCliForTest: (path: string, args: string[], options?: SpawnSyncOptionsWithStringEncoding) => child_process0.SpawnSyncReturns<string>;
63
+ /** 判断测试文件(夹) */
64
+ declare const isTestFile: (name: string) => boolean;
65
+ /** 解析 Github 链接获取 owner 和 repo */
66
+ declare const parseGitHubRepo: (url: string) => string[];
67
+ /** 基于 EOL 的可多换行函数 */
68
+ declare const eol: (n?: number) => string;
69
+ //#endregion
70
+ //#region src/shell.d.ts
71
+ declare const dim: (text: string) => string;
72
+ declare const red: (text: string) => string;
73
+ declare const yellow: (text: string) => string;
74
+ declare const underline: (text: string) => string;
75
+ declare const spawnAsync: <T = SpawnOptions>(cmd: string, args: string[], options?: SpawnAsyncOptions<T>) => Promise<string | undefined>;
76
+ declare const execAsync: <T = ExecOptions>(cmd: string, options?: ExecAsyncOptions<T>) => Promise<string | undefined>;
77
+ declare const runGit: (args: string[], options?: SpawnAsyncOptions) => Promise<string | undefined>;
78
+ declare const runNpm: (args: string[], options?: ExecAsyncOptions) => Promise<string | undefined>;
79
+ declare const fixArgs: (args: string) => string[];
80
+ declare const checkVersion: (cmd: string) => Promise<string | undefined>;
81
+ //#endregion
82
+ //#region src/joinUrl.d.ts
83
+ declare function joinUrl(...args: string[]): string;
84
+ declare function joinUrl(input: readonly string[]): string;
85
+ //#endregion
86
+ //#region src/git.d.ts
87
+ declare const isGitRepo: (dir?: string) => Promise<boolean>;
88
+ declare const getGitConfig: (key: string, global?: boolean) => Promise<string | undefined>;
89
+ declare const getGitRemoteUrl: (remoteName?: string) => Promise<string | undefined>;
90
+ //#endregion
91
+ //#region src/npm.d.ts
92
+ declare const pkgVersion: (pkg: string) => Promise<string | undefined>;
66
93
  //#endregion
67
- export { CliOptions, ConfirmResult, CopyOptions, HttpLibrary, PkgInfo, PkgManager, YesOrNo, checkVersion, copyDirAsync, editFile, editJsonFile, emptyDir, execAsync, getGitConfig, isEmpty, isGitRepo, isValidPackageName, pkgFromUserAgent, pkgVersion, readJsonFile, readSubDirs, runCli, toValidPackageName, toValidProjectName };
94
+ export { CliOptions, ConfirmResult, CopyOptions, ExecAsyncOptions, ExecResultOptions, HttpLibrary, PkgInfo, PkgManager, SpawnAsyncOptions, YesOrNo, checkVersion, copyDirAsync, dim, editFile, editJsonFile, emptyDir, eol, execAsync, fixArgs, getGitConfig, getGitRemoteUrl, isEmpty, isGitRepo, isTestFile, isValidPackageName, joinUrl, parseGitHubRepo, pkgFromUserAgent, pkgVersion, readJsonFile, readSubDirs, red, runCliForTest, runGit, runNpm, spawnAsync, toValidPackageName, toValidProjectName, underline, yellow };
@@ -1,5 +1,5 @@
1
1
  import * as child_process0 from "child_process";
2
- import { SpawnSyncOptionsWithStringEncoding } from "child_process";
2
+ import { ExecOptions, SpawnOptions, SpawnSyncOptionsWithStringEncoding } from "child_process";
3
3
 
4
4
  //#region src/enums.d.ts
5
5
  declare enum PkgManager {
@@ -37,11 +37,15 @@ interface PkgInfo {
37
37
  name: string;
38
38
  version: string;
39
39
  }
40
+ interface ExecResultOptions {
41
+ trim?: boolean;
42
+ dryRun?: boolean;
43
+ error?: 'log' | 'throw' | 'ignore';
44
+ }
45
+ type SpawnAsyncOptions<T = SpawnOptions> = T & ExecResultOptions;
46
+ type ExecAsyncOptions<T = ExecOptions> = T & ExecResultOptions;
40
47
  //#endregion
41
48
  //#region src/utils.d.ts
42
- declare const execAsync: (cmd: string) => Promise<string | undefined>;
43
- declare const pkgVersion: (pkg: string) => Promise<string | undefined>;
44
- declare const checkVersion: (cmd: string) => Promise<string | undefined>;
45
49
  declare const isValidPackageName: (packageName: string) => boolean;
46
50
  declare const toValidPackageName: (packageName: string) => string;
47
51
  declare const toValidProjectName: (projectName: string) => string;
@@ -49,19 +53,42 @@ declare const emptyDir: (dir: string, ignore?: string[]) => Promise<boolean>;
49
53
  declare const isEmpty: (path: string, ignore?: string[]) => Promise<boolean>;
50
54
  declare const editFile: (file: string, callback: (content: string) => string) => Promise<void>;
51
55
  declare const editJsonFile: <T extends Record<string, any>>(file: string, callback: (json: T) => void) => Promise<void>;
52
- declare const isGitRepo: (dir?: string) => Promise<boolean>;
53
56
  declare const readSubDirs: (source: string, ignore?: string[]) => Promise<string[]>;
54
- declare const getGitConfig: (key: string, global?: boolean) => Promise<string | undefined>;
55
57
  declare const copyDirAsync: (src: string, dest: string, options: CopyOptions) => Promise<void>;
56
58
  declare const readJsonFile: <T extends Record<string, any>>(file: string) => T;
57
- /**
58
- * 通过包管理器执行脚本时生效
59
- *
60
- * UserAgent: `process.env.npm_config_user_agent`
61
- * @param {string | undefined} userAgent
62
- * @returns {PkgInfo | undefined}
63
- */
59
+ /** 通过包管理器执行脚本时生效 UserAgent: `process.env.npm_config_user_agent` */
64
60
  declare const pkgFromUserAgent: (userAgent: string | undefined) => PkgInfo | undefined;
65
- declare const runCli: (path: string, args: string[], options?: SpawnSyncOptionsWithStringEncoding) => child_process0.SpawnSyncReturns<string>;
61
+ /** 同步执行 Node CLI(用于测试环境) */
62
+ declare const runCliForTest: (path: string, args: string[], options?: SpawnSyncOptionsWithStringEncoding) => child_process0.SpawnSyncReturns<string>;
63
+ /** 判断测试文件(夹) */
64
+ declare const isTestFile: (name: string) => boolean;
65
+ /** 解析 Github 链接获取 owner 和 repo */
66
+ declare const parseGitHubRepo: (url: string) => string[];
67
+ /** 基于 EOL 的可多换行函数 */
68
+ declare const eol: (n?: number) => string;
69
+ //#endregion
70
+ //#region src/shell.d.ts
71
+ declare const dim: (text: string) => string;
72
+ declare const red: (text: string) => string;
73
+ declare const yellow: (text: string) => string;
74
+ declare const underline: (text: string) => string;
75
+ declare const spawnAsync: <T = SpawnOptions>(cmd: string, args: string[], options?: SpawnAsyncOptions<T>) => Promise<string | undefined>;
76
+ declare const execAsync: <T = ExecOptions>(cmd: string, options?: ExecAsyncOptions<T>) => Promise<string | undefined>;
77
+ declare const runGit: (args: string[], options?: SpawnAsyncOptions) => Promise<string | undefined>;
78
+ declare const runNpm: (args: string[], options?: ExecAsyncOptions) => Promise<string | undefined>;
79
+ declare const fixArgs: (args: string) => string[];
80
+ declare const checkVersion: (cmd: string) => Promise<string | undefined>;
81
+ //#endregion
82
+ //#region src/joinUrl.d.ts
83
+ declare function joinUrl(...args: string[]): string;
84
+ declare function joinUrl(input: readonly string[]): string;
85
+ //#endregion
86
+ //#region src/git.d.ts
87
+ declare const isGitRepo: (dir?: string) => Promise<boolean>;
88
+ declare const getGitConfig: (key: string, global?: boolean) => Promise<string | undefined>;
89
+ declare const getGitRemoteUrl: (remoteName?: string) => Promise<string | undefined>;
90
+ //#endregion
91
+ //#region src/npm.d.ts
92
+ declare const pkgVersion: (pkg: string) => Promise<string | undefined>;
66
93
  //#endregion
67
- export { CliOptions, ConfirmResult, CopyOptions, HttpLibrary, PkgInfo, PkgManager, YesOrNo, checkVersion, copyDirAsync, editFile, editJsonFile, emptyDir, execAsync, getGitConfig, isEmpty, isGitRepo, isValidPackageName, pkgFromUserAgent, pkgVersion, readJsonFile, readSubDirs, runCli, toValidPackageName, toValidProjectName };
94
+ export { CliOptions, ConfirmResult, CopyOptions, ExecAsyncOptions, ExecResultOptions, HttpLibrary, PkgInfo, PkgManager, SpawnAsyncOptions, YesOrNo, checkVersion, copyDirAsync, dim, editFile, editJsonFile, emptyDir, eol, execAsync, fixArgs, getGitConfig, getGitRemoteUrl, isEmpty, isGitRepo, isTestFile, isValidPackageName, joinUrl, parseGitHubRepo, pkgFromUserAgent, pkgVersion, readJsonFile, readSubDirs, red, runCliForTest, runGit, runNpm, spawnAsync, toValidPackageName, toValidProjectName, underline, yellow };
@@ -1,7 +1,9 @@
1
- import { exec, spawnSync } from "node:child_process";
1
+ import { exec, 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";
6
+ import { styleText } from "node:util";
5
7
 
6
8
  //#region src/enums.ts
7
9
  let PkgManager = /* @__PURE__ */ function(PkgManager$1) {
@@ -35,17 +37,6 @@ let HttpLibrary = /* @__PURE__ */ function(HttpLibrary$1) {
35
37
 
36
38
  //#endregion
37
39
  //#region src/utils.ts
38
- const execAsync = (cmd) => {
39
- return new Promise((r) => {
40
- exec(cmd, (err, stdout) => r(err ? void 0 : stdout.trim()));
41
- });
42
- };
43
- const pkgVersion = (pkg) => {
44
- return execAsync(`npm view ${pkg} version`);
45
- };
46
- const checkVersion = async (cmd) => {
47
- return execAsync(`${cmd} --version`);
48
- };
49
40
  const isValidPackageName = (packageName) => {
50
41
  return /^(?:@[a-z\d\-*~][a-z\d\-*._~]*\/)?[a-z\d\-~][a-z\d\-._~]*$/.test(packageName);
51
42
  };
@@ -81,15 +72,9 @@ const editJsonFile = (file, callback) => {
81
72
  }
82
73
  });
83
74
  };
84
- const isGitRepo = async (dir) => {
85
- return !!await execAsync(`git -C "${dir ? `./${dir}` : "."}" rev-parse --is-inside-work-tree`);
86
- };
87
75
  const readSubDirs = async (source, ignore = []) => {
88
76
  return (await readdir(source, { withFileTypes: true })).filter((k) => k.isDirectory() && !ignore.includes(k.name)).map((dir) => dir.name);
89
77
  };
90
- const getGitConfig = (key, global = true) => {
91
- return execAsync(`git config ${global ? "--global" : ""} ${key}`);
92
- };
93
78
  const copyDirAsync = async (src, dest, options) => {
94
79
  await mkdir(dest, { recursive: true });
95
80
  const entries = await readdir(src, { withFileTypes: true });
@@ -113,13 +98,7 @@ const readJsonFile = (file) => {
113
98
  return {};
114
99
  }
115
100
  };
116
- /**
117
- * 通过包管理器执行脚本时生效
118
- *
119
- * UserAgent: `process.env.npm_config_user_agent`
120
- * @param {string | undefined} userAgent
121
- * @returns {PkgInfo | undefined}
122
- */
101
+ /** 通过包管理器执行脚本时生效 UserAgent: `process.env.npm_config_user_agent` */
123
102
  const pkgFromUserAgent = (userAgent) => {
124
103
  if (!userAgent) return void 0;
125
104
  const [name, version] = userAgent.split(" ")[0].split("/");
@@ -128,7 +107,8 @@ const pkgFromUserAgent = (userAgent) => {
128
107
  version
129
108
  };
130
109
  };
131
- const runCli = (path, args, options) => {
110
+ /** 同步执行 Node CLI(用于测试环境) */
111
+ const runCliForTest = (path, args, options) => {
132
112
  return spawnSync("node", [path, ...args], {
133
113
  env: {
134
114
  ...process.env,
@@ -138,6 +118,158 @@ const runCli = (path, args, options) => {
138
118
  ...options
139
119
  });
140
120
  };
121
+ /** 判断测试文件(夹) */
122
+ const isTestFile = (name) => {
123
+ return [
124
+ /(^|[\\/])(test(s?)|__test(s?)__)([\\/]|$)/,
125
+ /\.([a-zA-Z0-9]+-)?(test|spec)\.m?(ts|js)$/,
126
+ /^vitest([-.])(.*)\.m?(ts|js)$/
127
+ ].some((reg) => reg.test(name));
128
+ };
129
+ /** 解析 Github 链接获取 owner 和 repo */
130
+ const parseGitHubRepo = (url) => {
131
+ const match = url.trim().match(/github(?:\.com)?[:/](.+?)\/(.+?)(?:[#/?].+?)?(?:\.git)?$/);
132
+ return match ? match.slice(1, 3) : [];
133
+ };
134
+ /** 基于 EOL 的可多换行函数 */
135
+ const eol = (n = 1) => EOL.repeat(n);
136
+
137
+ //#endregion
138
+ //#region src/shell.ts
139
+ const dim = (text) => styleText(["dim"], text);
140
+ const red = (text) => styleText(["red"], text);
141
+ const yellow = (text) => styleText(["yellow"], text);
142
+ const underline = (text) => styleText(["underline"], text);
143
+ const spawnAsync = (cmd, args, options) => {
144
+ return new Promise((resolve$1) => {
145
+ const { trim, error, dryRun, ...others } = options ?? {};
146
+ const fullCmd = [cmd, ...args].join(" ");
147
+ if (dryRun) {
148
+ console.log(`${dim("[dry-run]")} ${fullCmd}`);
149
+ return resolve$1(void 0);
150
+ }
151
+ const child = spawn(cmd, args, { ...others });
152
+ let stdout = "";
153
+ child.stdout?.setEncoding("utf-8");
154
+ child.stdout?.on("data", (data) => stdout += trim ? data.trim() : data);
155
+ let stderr = "";
156
+ child.stderr?.setEncoding("utf-8");
157
+ child.stderr?.on("data", (data) => stderr += trim ? data.trim() : data);
158
+ child.on("close", (code) => {
159
+ if (stderr) {
160
+ const err = `${red("spawnAsync")} ${dim(fullCmd)} ${stderr}`;
161
+ switch (error) {
162
+ case "log":
163
+ console.error(err);
164
+ break;
165
+ case "throw": throw new Error(err);
166
+ }
167
+ }
168
+ resolve$1(0 === code ? stdout : void 0);
169
+ });
170
+ });
171
+ };
172
+ const execAsync = (cmd, options) => {
173
+ return new Promise((resolve$1) => {
174
+ const { trim, dryRun, error, ...others } = options ?? {};
175
+ if (dryRun) {
176
+ console.log(`${dim("[dry-run]")} ${cmd}`);
177
+ return resolve$1(void 0);
178
+ }
179
+ exec(cmd, { ...others }, (err, stdout) => {
180
+ if (err) {
181
+ const msg = `${red("execAsync")} ${dim(cmd)} ${err.message}`;
182
+ switch (error) {
183
+ case "log":
184
+ console.error(msg);
185
+ break;
186
+ case "throw": throw new Error(msg);
187
+ }
188
+ }
189
+ resolve$1(err ? void 0 : trim ? stdout.trim() : stdout);
190
+ });
191
+ });
192
+ };
193
+ const runGit = async (args, options = { trim: true }) => {
194
+ return spawnAsync("git", args, options);
195
+ };
196
+ const runNpm = (args, options = { trim: true }) => {
197
+ return execAsync(["npm", ...args].join(" "), options);
198
+ };
199
+ const fixArgs = (args) => args.trim() ? args.trim().split(" ") : [];
200
+ const checkVersion = async (cmd) => {
201
+ return execAsync(`${cmd} --version`);
202
+ };
203
+
204
+ //#endregion
205
+ //#region src/joinUrl.ts
206
+ function joinUrl(input) {
207
+ const temps = Array.isArray(input) ? input : [...arguments];
208
+ if (temps.length === 0) return "";
209
+ const result = [];
210
+ const parts = [...temps];
211
+ /** 协议正则 */
212
+ const PROTOCOL_RE = /^[^/:]+:\/*$/;
213
+ const FILE_PROTOCOL_RE = /^file:\/\/\//;
214
+ if (PROTOCOL_RE.test(parts[0]) && parts.length > 1) {
215
+ parts[1] = parts[0] + parts[1];
216
+ parts.shift();
217
+ }
218
+ if (FILE_PROTOCOL_RE.test(parts[0])) parts[0] = parts[0].replace(/^([^/:]+):\/*/, "$1:///");
219
+ else parts[0] = parts[0].replace(/^([^/:]+):\/*/, "$1://");
220
+ parts.forEach((part, index) => {
221
+ if (!part) return;
222
+ let segment = part;
223
+ if (index > 0) segment = segment.replace(/^\/+/, "");
224
+ if (index < parts.length - 1) segment = segment.replace(/\/+$/, "");
225
+ else segment = segment.replace(/\/+$/, "/");
226
+ result.push(segment);
227
+ });
228
+ let url = result.join("/");
229
+ url = url.replace(/\/(\?|&|#[^!])/g, "$1");
230
+ const [base, ...queryParts] = url.split("?");
231
+ url = base + (queryParts.length ? "?" + queryParts.join("&") : "");
232
+ return url;
233
+ }
234
+
235
+ //#endregion
236
+ //#region src/git.ts
237
+ const isGitRepo = async (dir) => {
238
+ return !!await runGit([
239
+ "-C",
240
+ dir ? `./${dir}` : ".",
241
+ "rev-parse",
242
+ "--is-inside-work-tree"
243
+ ]);
244
+ };
245
+ const getGitConfig = (key, global = true) => {
246
+ return runGit([
247
+ "config",
248
+ ...global ? ["--global"] : [],
249
+ key
250
+ ]);
251
+ };
252
+ const getGitRemoteUrl = async (remoteName = "origin") => {
253
+ return runGit([
254
+ "remote",
255
+ "get-url",
256
+ remoteName
257
+ ]).catch((_) => runGit([
258
+ "config",
259
+ "--get",
260
+ `remote.${remoteName}.url`
261
+ ]));
262
+ };
263
+
264
+ //#endregion
265
+ //#region src/npm.ts
266
+ const pkgVersion = (pkg) => {
267
+ return runNpm([
268
+ "view",
269
+ pkg,
270
+ "version"
271
+ ]);
272
+ };
141
273
 
142
274
  //#endregion
143
- export { ConfirmResult, HttpLibrary, PkgManager, YesOrNo, checkVersion, copyDirAsync, editFile, editJsonFile, emptyDir, execAsync, getGitConfig, isEmpty, isGitRepo, isValidPackageName, pkgFromUserAgent, pkgVersion, readJsonFile, readSubDirs, runCli, toValidPackageName, toValidProjectName };
275
+ export { ConfirmResult, HttpLibrary, PkgManager, YesOrNo, checkVersion, copyDirAsync, dim, editFile, editJsonFile, emptyDir, eol, execAsync, fixArgs, getGitConfig, getGitRemoteUrl, isEmpty, isGitRepo, isTestFile, isValidPackageName, joinUrl, parseGitHubRepo, pkgFromUserAgent, pkgVersion, readJsonFile, readSubDirs, red, runCliForTest, runGit, runNpm, spawnAsync, toValidPackageName, toValidProjectName, underline, yellow };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peiyanlu/cli-utils",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Shared utils for building interactive Node.js CLI applications.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -56,7 +56,6 @@
56
56
  "prepublish": "tsdown",
57
57
  "release": "release-it",
58
58
  "test": "vitest run",
59
- "test:e2e": "vitest run -c vitest.config.e2e.mts",
60
59
  "test:cov": "vitest run --coverage"
61
60
  }
62
61
  }