mixcli 3.2.0 → 3.2.2

Sign up to get free protection for your applications and to get access to all the features.
package/src/command.ts CHANGED
@@ -1,452 +1,452 @@
1
- import { Command, Option } from "commander";
2
- import prompts, { PromptObject } from "prompts";
3
- import { MixOption, type MixedOptionParams } from "./option";
4
- import { addBuiltInOptions, isEnablePrompts, outputDebug } from "./utils";
5
- import path from "node:path";
6
- import fs from "node:fs";
7
- import type { AsyncFunction } from "flex-tools";
8
-
9
- export type ICommandHookListener = ({
10
- args,
11
- options,
12
- command,
13
- }: {
14
- args: any[];
15
- options: Record<string, any>;
16
- command: MixCommand;
17
- }) => void | Promise<void>;
18
-
19
- export type BeforeCommandHookListener = ({
20
- args,
21
- options,
22
- command,
23
- }: {
24
- args: any[];
25
- options: Record<string, any>;
26
- command: MixCommand;
27
- }) => void | Promise<void>;
28
- export type AfterCommandHookListener = ({
29
- value,
30
- args,
31
- options,
32
- command,
33
- }: {
34
- value: any;
35
- args: any[];
36
- options: Record<string, any>;
37
- command: MixCommand;
38
- }) => void | Promise<void>;
39
-
40
- export interface ActionOptions {
41
- id: string;
42
- at: "replace" | "before" | "after" | "preappend" | "append" | number;
43
- // 函数签名类型,即采用原始的commander的action函数签名,还是mixcli的action函数签名
44
- enhance: boolean;
45
- }
46
-
47
- export interface ActionRegistry extends Omit<ActionOptions, "at"> {
48
- fn: Function;
49
- }
50
-
51
- // 原始的Action动作函数
52
- export type OriginalAction = (...args: any[]) => void | Promise<void>;
53
- // 增强的Action函数签名
54
- export type EnhanceAction = ({
55
- args,
56
- options,
57
- value,
58
- command,
59
- }: {
60
- args: any[];
61
- options: Record<string, any>;
62
- value: any;
63
- command: MixCommand;
64
- }) => void | Promise<any>;
65
-
66
- // 执行action的返回结果
67
- export const BREAK = Symbol("BREAK_ACTION"); // 中止后续的action执行
68
-
69
- export class MixCommand extends Command {
70
- __MIX_COMMAND__ = true;
71
- private _beforeHooks: [BeforeCommandHookListener, boolean][] = [];
72
- private _afterHooks: [AfterCommandHookListener, boolean][] = [];
73
- private _customPrompts: PromptObject[] = [];
74
- private _optionValues: Record<string, any> = {}; // 命令行输入的选项值
75
- private _actions: ActionRegistry[] = []; // 允许多个action
76
- private _enable_prompts: boolean = true; // 是否启用交互提示
77
- constructor(name?: string) {
78
- super(name);
79
- const self = this;
80
- if (!this.isRoot) addBuiltInOptions(this);
81
- this.hook("preAction", async function (this: any) {
82
- self._optionValues = self.getOptionValues(this.hookedCommand);
83
- try {
84
- // @ts-ignore
85
- await self.preActionHook.apply(self, arguments);
86
- } catch {}
87
- });
88
- }
89
- /**
90
- * 是否是根命令
91
- */
92
- get isRoot() {
93
- return !!!this.parent;
94
- }
95
- get actions() {
96
- return this._actions;
97
- }
98
- get beforeHooks() {
99
- return this._beforeHooks;
100
- }
101
- get afterHooks() {
102
- return this._afterHooks;
103
- }
104
- get fullname() {
105
- let names = [this.name()];
106
- let parent = this.parent;
107
- while (parent) {
108
- if (parent.name() !== "root") {
109
- names.unshift(parent.name());
110
- }
111
- parent = parent.parent;
112
- }
113
- return names.join(".");
114
- }
115
-
116
- /**
117
- * 返回根命令
118
- */
119
- root() {
120
- let root: MixCommand | null | undefined = this;
121
- while (root && root.parent != null) {
122
- root = root.parent as unknown as MixCommand;
123
- }
124
- return root;
125
- }
126
- action(fn: EnhanceAction, options: ActionOptions): this;
127
- action(fn: OriginalAction): this;
128
- action(fn: OriginalAction): this {
129
- const actionFunc = arguments[0];
130
- if (arguments.length == 1 && typeof actionFunc == "function") {
131
- // 原始方式
132
- this._actions.push({
133
- id: Math.random().toString(36).substring(2),
134
- enhance: false,
135
- fn: actionFunc,
136
- });
137
- } else if (
138
- arguments.length == 2 &&
139
- typeof actionFunc == "function" &&
140
- typeof arguments[1] == "object"
141
- ) {
142
- // 增强模式
143
- const actionFn = arguments[0];
144
- const actionOpts: ActionOptions = Object.assign({ at: "append" }, arguments[1]);
145
- if (actionOpts.at == "replace") this._actions = [];
146
- const actionItem = {
147
- id: actionOpts.id || Math.random().toString(36).substring(2),
148
- enhance: actionOpts.enhance == undefined ? true : actionOpts.enhance,
149
- fn: actionFn,
150
- } as const;
151
- if (typeof actionOpts.at == "number") {
152
- this._actions.splice(Number(actionOpts.at), 0, actionItem);
153
- } else if (["append", "before"].includes(actionOpts.at)) {
154
- this._actions.push(actionItem);
155
- } else if (["preappend", "after"].includes(actionOpts.at)) {
156
- this._actions.splice(0, 0, actionItem);
157
- } else {
158
- this._actions.push(actionItem);
159
- }
160
- } else {
161
- console.log("[mixcli] action params error");
162
- }
163
- return super.action(this.getWrapperedAction());
164
- }
165
-
166
- /**
167
- * 读取命令配置值,包括父命令提供的配置选项
168
- * @param command
169
- */
170
- private getOptionValues(command: Command) {
171
- let opts = {};
172
- let parent: Command | null = command;
173
- while (parent) {
174
- Object.assign(opts, (parent as MixCommand)._optionValues);
175
- parent = parent.parent;
176
- }
177
- return opts;
178
- }
179
- /**
180
- * 本函数在运行时子类进行action生成该命令的action
181
- */
182
- private getWrapperedAction() {
183
- return this.wrapperWorkDirsAction(this.wrapperChainActions());
184
- }
185
-
186
- /**
187
- * 向上查找所有祖先命令
188
- */
189
- private getAncestorCommands(): MixCommand[] {
190
- let cmds: MixCommand[] = [];
191
- let cmd: MixCommand | null = this;
192
- while (cmd) {
193
- cmd = cmd.parent as MixCommand;
194
- if (cmd) {
195
- cmds.push(cmd);
196
- }
197
- }
198
- return cmds;
199
- }
200
- /***
201
- * 将所有actions包装成一个链式调用的函数
202
- */
203
- private wrapperChainActions() {
204
- const self = this;
205
- return async function (this: any) {
206
- const args = Array.from(arguments); // 原始输入的参数
207
- let preValue: any; // 保存上一个action的返回值
208
- //解析参数, 0-1个参数为options,最后一个参数为command
209
- let actionOpts: Record<string, any> = {},
210
- actionArgs: any[] = [],
211
- cmd: any;
212
- if (args.length >= 2) {
213
- cmd = args[args.length - 1]; // 最后一个command
214
- actionOpts = args[args.length - 2];
215
- actionArgs = args.slice(0, args.length - 2);
216
- }
217
- await self.executeBeforeHooks({ args: actionArgs, options: actionOpts, command: cmd });
218
- try {
219
- for (let action of self._actions) {
220
- try {
221
- if (action.enhance) {
222
- // 增强模式
223
- outputDebug("执行<{}>: args={}, options={}", () => [
224
- self.name(),
225
- actionArgs,
226
- actionOpts,
227
- ]);
228
- preValue = await action.fn.call(this, {
229
- command: cmd,
230
- value: preValue,
231
- args: actionArgs,
232
- options: actionOpts,
233
- });
234
- } else {
235
- // 原始模式
236
- preValue = await action.fn.apply(this, args);
237
- }
238
- if (preValue === BREAK) break;
239
- } catch (e) {
240
- outputDebug("命令{}的Action({})执行出错:{}", [self.name, action.id, e]);
241
- throw e;
242
- }
243
- }
244
- } finally {
245
- await self.executeAfterHooks({
246
- value: preValue,
247
- args: actionArgs,
248
- options: actionOpts,
249
- command: cmd,
250
- });
251
- }
252
- };
253
- }
254
- /**
255
- * 当传入--work-dirs时用来处理工作目录
256
- */
257
- private wrapperWorkDirsAction(fn: AsyncFunction) {
258
- const self = this;
259
- return async function (this: any) {
260
- let workDirs = self._optionValues.workDirs;
261
- // 未指定工作目录参数
262
- if (!workDirs) {
263
- return await fn.apply(this, Array.from(arguments));
264
- }
265
- if (!Array.isArray(workDirs)) workDirs = workDirs.split(",");
266
- workDirs = workDirs.reduce((dirs: any[], dir: string) => {
267
- if (typeof dir == "string") dirs.push(...dir.split(","));
268
- return dirs;
269
- }, []);
270
- for (let workDir of workDirs) {
271
- const cwd = process.cwd();
272
- try {
273
- if (!path.isAbsolute(workDir)) workDir = path.join(cwd, workDir);
274
- if (fs.existsSync(workDir) && fs.statSync(workDir).isDirectory()) {
275
- outputDebug("切换到工作目录:{}", workDir);
276
- process.chdir(workDir); // 切换
277
- await fn.apply(this, Array.from(arguments));
278
- } else {
279
- outputDebug("无效的工作目录:{}", workDir);
280
- }
281
- } catch (e) {
282
- throw e;
283
- } finally {
284
- process.chdir(cwd);
285
- }
286
- }
287
- };
288
- }
289
- getOption(name: string): MixOption {
290
- return this.options.find((option) => option.name() == name) as unknown as MixOption;
291
- }
292
- /**
293
- * 添加一个Before钩子
294
- * @param listener
295
- * @param scope =false时代表只在本命令执行,=true时代表在本命令及其子命令执行
296
- * @returns
297
- */
298
- before(listener: BeforeCommandHookListener, scope: boolean = true) {
299
- this._beforeHooks.push([listener, scope]);
300
- return this;
301
- }
302
- private async executeBeforeHooks(args: any) {
303
- const hooks: [BeforeCommandHookListener, boolean, MixCommand][] = this.beforeHooks.map(
304
- ([hook, scope]) => [hook, scope, this]
305
- );
306
- this.getAncestorCommands().forEach((cmd: MixCommand) => {
307
- hooks.unshift(
308
- ...cmd.beforeHooks.map(([hook, scope]) => {
309
- return [hook, scope, cmd] as [BeforeCommandHookListener, boolean, MixCommand];
310
- })
311
- );
312
- });
313
- for (let [hook, scope, cmd] of hooks) {
314
- if (!scope) continue;
315
- await hook.call(cmd, args);
316
- }
317
- }
318
- /**
319
- * 添加一个After钩子
320
- * @param listener
321
- * @param scope =false时代表只在本命令执行,=true时代表在本命令及其子命令执行
322
- * @returns
323
- */
324
- after(listener: AfterCommandHookListener, scope: boolean = true) {
325
- this._afterHooks.push([listener, scope]);
326
- return this;
327
- }
328
- private async executeAfterHooks(args: any) {
329
- const hooks: [AfterCommandHookListener, boolean, MixCommand][] = this.afterHooks.map(
330
- ([hook, scope]) => [hook, scope, this]
331
- );
332
- this.getAncestorCommands().forEach((cmd: MixCommand) => {
333
- hooks.push(
334
- ...cmd.afterHooks.map(([hook, scope]) => {
335
- return [hook, scope, cmd] as [BeforeCommandHookListener, boolean, MixCommand];
336
- })
337
- );
338
- });
339
- for (let [hook, scope, cmd] of hooks) {
340
- if (!scope) continue; //=false时不执行
341
- await hook.call(cmd, args);
342
- }
343
- }
344
- private async preActionHook(thisCommand: Command, actionCommand: Command) {
345
- if (this.isEnablePrompts()) {
346
- // 自动生成提示
347
- const questions: PromptObject[] = [
348
- ...this.generateAutoPrompts(),
349
- ...this._customPrompts,
350
- ];
351
- // 用户提示
352
- if (questions.length > 0) {
353
- const results = await prompts(questions);
354
- Object.entries(results).forEach(([key, value]) => {
355
- thisCommand.setOptionValue(key, value);
356
- });
357
- }
358
- }
359
- }
360
-
361
- private isEnablePrompts() {
362
- if (isEnablePrompts() === false) {
363
- // 命令行参数禁用了提示,优先级最高
364
- return false;
365
- } else {
366
- return this._enable_prompts;
367
- }
368
- }
369
-
370
- /**
371
- * 生成选项自动提示
372
- *
373
- * @remarks
374
- * FlexCli要求所有未提供默认值的Option自动生成提示
375
- *
376
- * - 未提供默认值,并且是必选的参数Option
377
- * - 指定了choices但未提供有效值的Option
378
- *
379
- */
380
- private generateAutoPrompts(): PromptObject[] {
381
- const options = this.options as unknown as MixOption[];
382
- const optionPromports = options
383
- .filter((option) => !option.hidden && option.__MIX_OPTION__)
384
- .map((option) => option.getPrompt(this._optionValues[option.name()]))
385
- .filter((prompt) => prompt) as PromptObject[];
386
- outputDebug("命令<{}>自动生成{}个选项提示:{}", [
387
- this.name(),
388
- optionPromports.length,
389
- optionPromports.map((prompt) => `${prompt.name}(${prompt.type})`).join(","),
390
- ]);
391
- return optionPromports;
392
- }
393
- option(flags: string, description?: string | undefined, defaultValue?: any): this;
394
- option(flags: string, description?: string | undefined, options?: MixedOptionParams): this {
395
- // @ts-ignore
396
- const option = new MixOption(...arguments);
397
- if (option.required && !this.isEnablePrompts()) option.mandatory = true;
398
- return this.addOption(option as unknown as Option);
399
- }
400
-
401
- /**
402
- * 添加提示
403
- *
404
- * @remarks
405
- *
406
- * 添加一些自定义提示
407
- *
408
- *
409
- * @param questions
410
- * @param show 是否显示提示信息,auto表示只有在用户没有提供option的值时才显示提示信息,always表示总是显示提示信息,never表示不显示提示信息
411
- * @returns
412
- */
413
- prompt(questions: PromptObject | PromptObject[]) {
414
- this._customPrompts.push(...(Array.isArray(questions) ? questions : [questions]));
415
- return this;
416
- }
417
- /**
418
- *
419
- * 选择命令并执行
420
- *
421
- * @remorks
422
- *
423
- * 当命令具有多个子命令时,并且没有提供默认子命令时,提示用户选择一个子命令
424
- *
425
- */
426
- async selectCommands() {
427
- const choices = this.commands.map((command) => ({
428
- title: `${command.description()}(${command.name()})`,
429
- value: command.name(),
430
- }));
431
- const result = await prompts({
432
- type: "select",
433
- name: "command",
434
- message: "请选择命令:",
435
- choices,
436
- });
437
- // 重新解析命令行参数标志,
438
- const command = this.commands.find((command) => command.name() === result.command);
439
- await command?.parseAsync([result.command], { from: "user" });
440
- }
441
- /**
442
- * 禁用/启用所有提示
443
- */
444
- disablePrompts() {
445
- this._enable_prompts = false;
446
- return this;
447
- }
448
- enablePrompts() {
449
- this._enable_prompts = true;
450
- return this;
451
- }
452
- }
1
+ import { Command, Option } from "commander";
2
+ import prompts, { PromptObject } from "prompts";
3
+ import { MixOption, type MixedOptionParams } from "./option";
4
+ import { addBuiltInOptions, hyphenToCamelCase, isDisablePrompts, outputDebug } from "./utils";
5
+ import type { AsyncFunction } from "flex-tools/types";
6
+ import path from "node:path";
7
+ import fs from "node:fs";
8
+
9
+
10
+
11
+ export type IMixCommandHookListener = ({
12
+ args,
13
+ options,
14
+ command,
15
+ }: {
16
+ args: any[];
17
+ options: Record<string, any>;
18
+ command: MixCommand;
19
+ }) => void | Promise<void>;
20
+
21
+ export type BeforeMixCommandHookListener = ({
22
+ args,
23
+ options,
24
+ command,
25
+ }: {
26
+ args: any[];
27
+ options: Record<string, any>;
28
+ command: MixCommand;
29
+ }) => void | Promise<void>;
30
+
31
+ export type AfterMixCommandHookListener = ({
32
+ value,
33
+ args,
34
+ options,
35
+ command,
36
+ }: {
37
+ value: any;
38
+ args: any[];
39
+ options: Record<string, any>;
40
+ command: MixCommand;
41
+ }) => void | Promise<void>;
42
+
43
+ export interface MixActionOptions {
44
+ id: string;
45
+ at: "replace" | "before" | "after" | "preappend" | "append" | number;
46
+ enhance: boolean; // 函数签名类型,即采用原始的commander的action函数签名,还是mixcli的action函数签名
47
+ }
48
+
49
+ export interface MixActionRegistry extends Omit<MixActionOptions, "at"> {
50
+ fn: Function;
51
+ }
52
+
53
+ // 原始的Action动作函数
54
+ export type MixOriginalAction = (...args: any[]) => any | Promise<void>;
55
+
56
+ // 增强的Action函数签名
57
+ export type MixEnhanceAction = ({
58
+ args,
59
+ options,
60
+ value,
61
+ command,
62
+ }: {
63
+ args: any[];
64
+ options: Record<string, any>;
65
+ value: any;
66
+ command: MixCommand;
67
+ }) => any | Promise<any>;
68
+
69
+ // 执行action的返回结果
70
+ export const BREAK = Symbol("BREAK_ACTION"); // 中止后续的action执行
71
+
72
+ export class MixCommand extends Command {
73
+ __MIX_COMMAND__ = true;
74
+ private _beforeHooks : [ BeforeMixCommandHookListener, boolean ][] = [];
75
+ private _afterHooks : [ AfterMixCommandHookListener, boolean ][] = [];
76
+ private _customPrompts : PromptObject[] = [];
77
+ private _optionValues : Record<string, any> = {}; // 命令行输入的选项值
78
+ private _actions : MixActionRegistry[] = []; // 允许多个action
79
+ private _enable_prompts: boolean = true; // 是否启用交互提示
80
+ constructor(name?: string) {
81
+ super(name);
82
+ // eslint-disable-next-line no-this-alias
83
+ const self = this
84
+ if (!this.isRoot) addBuiltInOptions(this);
85
+ this.hook("preAction", async function (this: any) {
86
+ self._optionValues = self.getOptionValues(this.hookedCommand);
87
+ // @ts-ignore
88
+ await self.preActionHook.apply(self, arguments);
89
+ });
90
+ }
91
+ get isRoot() { return this.parent==undefined; }
92
+ get optionValues(){ return this._optionValues }
93
+ get actions() { return this._actions; }
94
+ get beforeHooks() { return this._beforeHooks; }
95
+ get afterHooks() {return this._afterHooks;}
96
+ get fullname() {
97
+ let names = [this.name()];
98
+ let parent = this.parent;
99
+ while (parent) {
100
+ if (parent.name() !== "root") {
101
+ names.unshift(parent.name());
102
+ }
103
+ parent = parent.parent;
104
+ }
105
+ return names.join(".");
106
+ }
107
+
108
+ /**
109
+ * 返回根命令
110
+ */
111
+ root() {
112
+ // eslint-disable-next-line no-this-alias
113
+ let root:any = this;
114
+ while (root && root.parent != null) {
115
+ root = root.parent as unknown as MixCommand;
116
+ }
117
+ return root;
118
+ }
119
+
120
+
121
+ action(fn: MixEnhanceAction, options: MixActionOptions): this;
122
+ action(fn: MixOriginalAction): this;
123
+ action(): this {
124
+ const actionFunc = arguments[0];
125
+ if (arguments.length == 1 && typeof actionFunc == "function") {
126
+ // 原始方式
127
+ this._actions.push({
128
+ id: Math.random().toString(36).substring(2),
129
+ enhance: false,
130
+ fn: actionFunc,
131
+ });
132
+ } else if (arguments.length == 2 && typeof actionFunc == "function" && typeof arguments[1] == "object" ) {
133
+ // 增强模式
134
+ const actionFn = arguments[0];
135
+ const actionOpts: MixActionOptions = Object.assign({ at: "append" }, arguments[1]);
136
+ if (actionOpts.at == "replace") this._actions = [];
137
+ const actionItem = {
138
+ id: actionOpts.id || Math.random().toString(36).substring(2),
139
+ enhance: actionOpts.enhance == undefined ? true : actionOpts.enhance,
140
+ fn: actionFn,
141
+ } as const;
142
+ if (typeof actionOpts.at == "number") {
143
+ this._actions.splice(Number(actionOpts.at), 0, actionItem);
144
+ } else if (["append", "before"].includes(actionOpts.at)) {
145
+ this._actions.push(actionItem);
146
+ } else if (["preappend", "after"].includes(actionOpts.at)) {
147
+ this._actions.splice(0, 0, actionItem);
148
+ } else {
149
+ this._actions.push(actionItem);
150
+ }
151
+ } else {
152
+ console.log("[mixcli] action params error");
153
+ }
154
+ return super.action(this.getWrapperedAction());
155
+ }
156
+
157
+ /**
158
+ * 读取命令配置值,包括父命令提供的配置选项
159
+ * @param command
160
+ */
161
+ private getOptionValues(command: MixCommand) {
162
+ const opts = {};
163
+ // eslint-disable-next-line no-this-alias
164
+ let parent: any = command;
165
+ while (parent) {
166
+ Object.assign(opts, (parent as MixCommand).optionValues);
167
+ parent = parent.parent;
168
+ }
169
+ return opts;
170
+ }
171
+ /**
172
+ * 本函数在运行时子类进行action生成该命令的action
173
+ */
174
+ private getWrapperedAction() {
175
+ return this.wrapperWorkDirsAction(this.wrapperChainActions());
176
+ }
177
+
178
+ /**
179
+ * 向上查找所有祖先命令
180
+ */
181
+ private getAncestorCommands(): MixCommand[] {
182
+ let cmds: MixCommand[] = [];
183
+ // eslint-disable-next-line no-this-alias
184
+ let cmd: MixCommand | null = this;
185
+ while (cmd) {
186
+ cmd = cmd.parent as unknown as MixCommand;
187
+ if (cmd) {
188
+ cmds.push(cmd);
189
+ }
190
+ }
191
+ return cmds;
192
+ }
193
+ /***
194
+ * 将所有actions包装成一个链式调用的函数
195
+ */
196
+ private wrapperChainActions() {
197
+ // eslint-disable-next-line no-this-alias
198
+ const self = this;
199
+ return async function (this: any) {
200
+ const args = Array.from(arguments); // 原始输入的参数
201
+ let preValue: any; // 保存上一个action的返回值
202
+ //解析参数, 0-1个参数为options,最后一个参数为command
203
+ let actionOpts: Record<string, any> = {},
204
+ actionArgs: any[] = [],
205
+ cmd: any;
206
+ if (args.length >= 2) {
207
+ cmd = args[args.length - 1]; // 最后一个command
208
+ actionOpts = args[args.length - 2];
209
+ actionArgs = args.slice(0, args.length - 2);
210
+ }
211
+ await self.executeBeforeHooks({ args: actionArgs, options: actionOpts, command: cmd });
212
+ try {
213
+ for (let action of self._actions) {
214
+ try {
215
+ if (action.enhance) {
216
+ // 增强模式
217
+ outputDebug("执行<{}>: args={}, options={}", () => [
218
+ self.name(),
219
+ actionArgs,
220
+ actionOpts,
221
+ ]);
222
+ preValue = await action.fn.call(this, {
223
+ command: cmd,
224
+ value : preValue,
225
+ args : actionArgs,
226
+ options: actionOpts,
227
+ });
228
+ } else {
229
+ // 原始模式
230
+ preValue = await action.fn.apply(this, args);
231
+ }
232
+ if (preValue === BREAK) break;
233
+ } catch (e) {
234
+ outputDebug("命令{}的Action({})执行出错:{}", [self.name, action.id, e]);
235
+ throw e;
236
+ }
237
+ }
238
+ } finally {
239
+ await self.executeAfterHooks({
240
+ value: preValue,
241
+ args: actionArgs,
242
+ options: actionOpts,
243
+ command: cmd,
244
+ });
245
+ }
246
+ };
247
+ }
248
+ /**
249
+ * 当传入--work-dirs时用来处理工作目录
250
+ */
251
+ private wrapperWorkDirsAction(fn: AsyncFunction) {
252
+ // eslint-disable-next-line no-this-alias
253
+ const self = this;
254
+ return async function (this: any) {
255
+ let workDirs = self._optionValues.workDirs;
256
+ // 未指定工作目录参数
257
+ if (!workDirs) {
258
+ return await fn.apply(this, Array.from(arguments));
259
+ }
260
+ if (!Array.isArray(workDirs)) workDirs = workDirs.split(",");
261
+ workDirs = workDirs.reduce((dirs: any[], dir: string) => {
262
+ if (typeof dir == "string") dirs.push(...dir.split(","));
263
+ return dirs;
264
+ }, []);
265
+ for (let workDir of workDirs) {
266
+ const cwd = process.cwd();
267
+ try {
268
+ if (!path.isAbsolute(workDir)) workDir = path.join(cwd, workDir);
269
+ if (fs.existsSync(workDir) && fs.statSync(workDir).isDirectory()) {
270
+ outputDebug("切换到工作目录:{}", workDir);
271
+ process.chdir(workDir); // 切换
272
+ await fn.apply(this, Array.from(arguments));
273
+ } else {
274
+ outputDebug("无效的工作目录:{}", workDir);
275
+ }
276
+ } finally {
277
+ process.chdir(cwd);
278
+ }
279
+ }
280
+ };
281
+ }
282
+ getOption(name: string): MixOption {
283
+ return this.options.find((option) => option.name() == name) as unknown as MixOption;
284
+ }
285
+ /**
286
+ * 添加一个Before钩子
287
+ * @param listener
288
+ * @param scope =false时代表只在本命令执行,=true时代表在本命令及其子命令执行
289
+ * @returns
290
+ */
291
+ before(listener: BeforeMixCommandHookListener, scope: boolean = true) {
292
+ this._beforeHooks.push([listener, scope]);
293
+ return this;
294
+ }
295
+ private async executeBeforeHooks(args: any) {
296
+ const hooks: [BeforeMixCommandHookListener, boolean, MixCommand][] = this.beforeHooks.map(
297
+ ([hook, scope]) => [hook, scope, this]
298
+ );
299
+ this.getAncestorCommands().forEach((cmd: MixCommand) => {
300
+ hooks.unshift(
301
+ ...cmd.beforeHooks.map(([hook, scope]) => {
302
+ return [hook, scope, cmd] as [BeforeMixCommandHookListener, boolean, MixCommand];
303
+ })
304
+ );
305
+ });
306
+ for (let [hook, scope, cmd] of hooks) {
307
+ if (!scope) continue;
308
+ await hook.call(cmd, args);
309
+ }
310
+ }
311
+ /**
312
+ * 添加一个After钩子
313
+ * @param listener
314
+ * @param scope =false时代表只在本命令执行,=true时代表在本命令及其子命令执行
315
+ * @returns
316
+ */
317
+ after(listener: AfterMixCommandHookListener, scope: boolean = true) {
318
+ this._afterHooks.push([listener, scope]);
319
+ return this;
320
+ }
321
+
322
+ private async executeAfterHooks(args: any) {
323
+ const hooks: [AfterMixCommandHookListener, boolean, MixCommand][] = this.afterHooks.map(
324
+ ([hook, scope]) => [hook, scope, this]
325
+ );
326
+ this.getAncestorCommands().forEach((cmd: MixCommand) => {
327
+ hooks.push(
328
+ ...cmd.afterHooks.map(([hook, scope]) => {
329
+ return [hook, scope, cmd] as [BeforeMixCommandHookListener, boolean, MixCommand];
330
+ })
331
+ );
332
+ });
333
+ for (let [hook, scope, cmd] of hooks) {
334
+ if (!scope) continue; //=false时不执行
335
+ await hook.call(cmd, args);
336
+ }
337
+ }
338
+ private async preActionHook(thisCommand: Command) {
339
+ if (this.isEnablePrompts()) {
340
+ // 自动生成提示
341
+ const questions: PromptObject[] = [
342
+ ...this.generateAutoPrompts(),
343
+ ...this._customPrompts,
344
+ ];
345
+ // 用户提示
346
+ if (questions.length > 0) {
347
+ const results = await prompts(questions);
348
+ Object.entries(results).forEach(([key, value]) => {
349
+ thisCommand.setOptionValue(key, value);
350
+ });
351
+ }
352
+ }
353
+ }
354
+
355
+ private isEnablePrompts() {
356
+ if (isDisablePrompts()) {
357
+ return false;// 命令行参数禁用了提示,优先级最高
358
+ } else {
359
+ return this._enable_prompts;
360
+ }
361
+ }
362
+
363
+ /**
364
+ * 生成选项自动提示
365
+ *
366
+ * @remarks
367
+ * 要求所有未提供默认值的Option自动生成提示
368
+ *
369
+ * - 未提供默认值,并且是必选的参数Option
370
+ * - 指定了choices但未提供有效值的Option
371
+ *
372
+ */
373
+ private generateAutoPrompts(): PromptObject[] {
374
+ const options = this.options as unknown as MixOption[];
375
+ const optionPromports = options
376
+ .filter((option) => !option.hidden && option.__MIX_OPTION__)
377
+ .map((option) => option.getPrompt(this._optionValues[option.attributeName()]))
378
+ .filter((prompt) => prompt) as PromptObject[];
379
+
380
+ outputDebug("命令<{}>自动生成{}个选项提示:{}", [
381
+ this.name(),
382
+ optionPromports.length,
383
+ optionPromports.map((prompt) => `${prompt.name}(${prompt.type})`).join(","),
384
+ ]);
385
+ return optionPromports;
386
+ }
387
+
388
+ // @ts-ignore
389
+ option( flags: string, description: string, options?: MixedOptionParams ):this{
390
+ const option = new MixOption(flags, description, options);
391
+ if (option.required && !this.isEnablePrompts()) option.mandatory = true;
392
+ return this.addOption(option as unknown as Option)
393
+ }
394
+
395
+ /**
396
+ * 添加提示
397
+ *
398
+ * @remarks
399
+ *
400
+ * 添加一些自定义提示
401
+ *
402
+ * @param questions
403
+ * @returns
404
+ */
405
+ prompt(questions: PromptObject | PromptObject[]) {
406
+ this._customPrompts.push(...(Array.isArray(questions) ? questions : [questions]));
407
+ return this;
408
+ }
409
+
410
+ /**
411
+ *
412
+ * 选择命令并执行
413
+ *
414
+ * @remorks
415
+ *
416
+ * 当命令具有多个子命令时,并且没有提供默认子命令时,提示用户选择一个子命令
417
+ *
418
+ */
419
+ async selectCommands() {
420
+ const choices = this.commands.map((command) => ({
421
+ title: `${command.description()}(${command.name()})`,
422
+ value: command.name(),
423
+ }));
424
+ const result = await prompts({
425
+ type : "select",
426
+ name : "command",
427
+ message: "请选择命令:",
428
+ choices,
429
+ });
430
+ // 重新解析命令行参数标志,
431
+ const command = this.commands.find((command) => command.name() === result.command);
432
+ await command?.parseAsync([result.command], { from: "user" });
433
+ }
434
+ /**
435
+ * 禁用/启用所有提示
436
+ */
437
+ disablePrompts() {
438
+ this._enable_prompts = false;
439
+ return this;
440
+ }
441
+ /**
442
+ * 启用所有提示
443
+ */
444
+ enablePrompts() {
445
+ this._enable_prompts = true;
446
+ return this;
447
+ }
448
+ }
449
+
450
+
451
+
452
+ // 编写一个类型号用来表示Commander的Option flags字面量类型