mixcli 3.0.10 → 3.2.1

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