mixcli 3.0.1 → 3.0.2

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/src/command.ts ADDED
@@ -0,0 +1,451 @@
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
+ }
package/src/finder.ts ADDED
@@ -0,0 +1,142 @@
1
+ import { getPackageRootPath } from 'flex-tools';
2
+ import type { MixCli } from './cli';
3
+ import { globSync } from 'glob'
4
+ import { MixCliCommand } from './cli';
5
+ import { isDebug, outputDebug } from './utils';
6
+ import fs from "node:fs"
7
+ import path from "node:path"
8
+ import { getPackageJson } from "flex-tools/package/getPackageJson"
9
+
10
+ /**
11
+ *
12
+ * 在当前工程中查找符合FlexCli.prefix约定的命令
13
+ *
14
+ * - 读取当前包的package.json
15
+ * - 找出所有以cli.prefix开头的依赖
16
+ * - 加载这些依赖的目录下的匹配cli.pattern的命令
17
+ * - 加载加载这样命令
18
+ *
19
+ */
20
+
21
+
22
+ export function getMatchedDependencies(this:MixCli,entry:string):string[]{
23
+ const pacakgeMacher = this.options.include
24
+ if(!(pacakgeMacher instanceof RegExp)) return []
25
+
26
+ // 找出当前包的所有依赖
27
+ const { dependencies={},devDependencies={},peerDependencies={},optionalDependencies={},bundleDependencies={} } = getPackageJson(entry)
28
+ const packageNames = [
29
+ ...Object.keys(dependencies),
30
+ ...Object.keys(devDependencies),
31
+ ...Object.keys(peerDependencies),
32
+ ...Object.keys(optionalDependencies),
33
+ ...Object.keys(bundleDependencies)
34
+ ]
35
+ return packageNames.filter(name=>name!=="@voerka/cli" && pacakgeMacher.test(name))
36
+ }
37
+
38
+ function isMatched(str:string,reg?:string | RegExp | string[] | RegExp[]):boolean{
39
+ // let regexps:RegExp[]=[]
40
+ const regexps = reg ? (Array.isArray(reg) ? reg : [reg]) : []
41
+ return regexps.some(regexp=>{
42
+ if(typeof regexp === "string"){
43
+ return (new RegExp(regexp)).test(str)
44
+ }else if(regexp instanceof RegExp){
45
+ return regexp.test(str)
46
+ }else{
47
+ return false
48
+ }
49
+ })
50
+ }
51
+
52
+ export function findCliPaths(this:MixCli,packageName?:string ,entry?:string):string[]{
53
+ const includeMacher = this.options.include
54
+ const excludeMacher = this.options.exclude
55
+ if(!includeMacher) return []
56
+ const packageRoot = getPackageRootPath(entry || process.cwd())
57
+ const packagePath = packageName ? path.dirname(require.resolve(packageName,{paths:[packageRoot as string]})) : packageName!
58
+
59
+ // 找出当前包的所有依赖
60
+ const packageNames = getMatchedDependencies.call(this,packagePath)
61
+
62
+ const cliDirs:string[]=[]
63
+
64
+ if(entry!==undefined) cliDirs.push(path.join(packagePath,this.options.cliDir))
65
+ packageNames.filter(name=>{
66
+ return isMatched(name,includeMacher) && !isMatched(name,excludeMacher)
67
+ })
68
+ .forEach(name=>{
69
+ outputDebug("匹配包:{}",`${packageName ? name+" <- "+packageName : name}`)
70
+ try{
71
+ const packageEntry = path.dirname(require.resolve(name,{paths:packagePath ? [packagePath] : [process.cwd()]}))
72
+ const packageCliDir =path.join(packageEntry,this.options.cliDir!)
73
+ // 查找当前包的所属工程的依赖
74
+ let dependencies = getMatchedDependencies.call(this,packageEntry)
75
+ cliDirs.push(...dependencies.reduce<string[]>((result,dependencie)=>{
76
+ outputDebug("匹配包:{}",`${dependencie} <- ${name}`)
77
+ result.push(...findCliPaths.call(this,dependencie,packageEntry))
78
+ return result
79
+ },[]))
80
+ if(fs.existsSync(packageCliDir)){
81
+ cliDirs.push(packageCliDir)
82
+ }
83
+ }catch(e:any){
84
+ outputDebug("解析包<{}>路径出错:{}",[name,e.stack])
85
+ }
86
+ })
87
+ // 由于一些包可能存在循环依赖,所以需要去重
88
+ return [...new Set(cliDirs)]
89
+ }
90
+
91
+
92
+ function showError(e:any){
93
+ if(isDebug()){
94
+ outputDebug("导入命令<>出错:{}",e.stack)
95
+ }else{
96
+ console.error(e)
97
+ }
98
+
99
+ }
100
+
101
+ /**
102
+ *
103
+ * 扫描当前工程中所有符合条件的命令
104
+ *
105
+ * @param cli
106
+ *
107
+ */
108
+ export async function findCommands(cli:MixCli){
109
+ const cliDirs = findCliPaths.call(cli)
110
+ const commands:MixCliCommand[] = []
111
+ const files = [] as string[]
112
+ cliDirs.forEach(dir=>{
113
+ files.push(...globSync("*",{
114
+ cwd:dir,
115
+ absolute :true
116
+ }).filter((file:string)=>(file.endsWith(".js") || file.endsWith(".cjs") || file.endsWith(".mjs")) && !fs.statSync(file).isDirectory()))
117
+ })
118
+ for(let file of files){
119
+ try{
120
+ outputDebug("导入命令:{}",file)
121
+ if(file.endsWith(".cjs") || file.endsWith(".js")){
122
+ commands.push(require(file))
123
+ }else if(file.endsWith(".mjs")){
124
+ const cmd = await import(`file://${file}`)
125
+ commands.push(cmd.default)
126
+ }
127
+ }catch(e:any){
128
+ if(e.code==="ERR_REQUIRE_ESM"){
129
+ try{
130
+ const cmd = await import(`file://${file.replace(".js",".mjs")}`)
131
+ commands.push(cmd.default)
132
+ }catch(err){
133
+ showError(err)
134
+ }
135
+ }else{
136
+ showError(e)
137
+ }
138
+ }
139
+ }
140
+ return commands
141
+ }
142
+
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from "./cli"
2
+ export * from "./utils"
3
+ export * from "./command"
4
+ export * from "./option"
package/src/option.ts ADDED
@@ -0,0 +1,102 @@
1
+ import { Option } from 'commander'
2
+ import { PromptObject } from 'prompts'
3
+ import { IPromptable, IPromptableOptions, PromptChoice, PromptManager } from './prompt'
4
+
5
+
6
+ export interface MixedOptionParams extends IPromptableOptions{
7
+ hidden?:boolean
8
+ defaultDescription?:string // 默认值的描述
9
+ conflicts?:string | string[]
10
+ env?:string
11
+ argParser?:<T>(value: string, previous: T) => T
12
+ hideHelp?:boolean
13
+ mandatory?: boolean
14
+ implies?:{[key:string]:any}
15
+ }
16
+
17
+
18
+ export class MixOption extends Option implements IPromptable{
19
+ // 是否提示用户输入
20
+ prompt?: PromptManager
21
+ promptChoices?:PromptChoice[]
22
+ private _validate?: (value: any) => boolean
23
+ constructor(flags: string, description?: string | undefined,optsOrDefault?:any) {
24
+ super(flags, description)
25
+ let params:MixedOptionParams = {}
26
+ if(arguments.length==3 && typeof arguments[2] == "object"){
27
+ params = Object.assign({ },arguments[2])
28
+ }else if(arguments.length==3){
29
+ params.default = arguments[2]
30
+ }
31
+ if(params.prompt===undefined) params.prompt = 'auto'
32
+ if(params.default) this.default(params.default,params.defaultDescription)
33
+ if(params.choices) this.choices(params.choices)
34
+ if(params.conflicts) this.conflicts(params.conflicts)
35
+ if(params.env) this.env(params.env)
36
+ if(params.argParser) this.argParser(params.argParser)
37
+ if(params.hideHelp) this.hideHelp(params.hideHelp)
38
+ if(params.hidden) this.hidden = params.hidden
39
+ if(params.mandatory) this.makeOptionMandatory(params.mandatory)
40
+ if(params.implies) this.implies(params.implies)
41
+ if(params.optional) this.optional=params.optional
42
+ if(typeof(params.validate)=='function') this._validate = params.validate.bind(this)
43
+ if(params.required) {
44
+ this.required = params.required
45
+ if(!this._validate ) this._validate = (value:any)=>String(value).length>0
46
+ }
47
+ this.prompt = new PromptManager(this as IPromptable,params.prompt)
48
+ }
49
+ validate(value: any): boolean {
50
+ if(typeof(this._validate)=='function'){
51
+ return this._validate(value)
52
+ }else{
53
+ return true
54
+ }
55
+ }
56
+ // @ts-ignore
57
+ choices(values:(PromptChoice | string)[]){
58
+ if(!this.promptChoices){
59
+ this.promptChoices = values.map(choice=>{
60
+ if(typeof(choice)=='object'){
61
+ return choice
62
+ }else{
63
+ return {title:choice,value:choice}
64
+ }
65
+ })
66
+ }
67
+ super.choices(this.promptChoices.map((item:any)=>item.value))
68
+ }
69
+
70
+ private resetChoices(){
71
+ super.choices(this.promptChoices!.map((item:any)=>item.value))
72
+ }
73
+
74
+ addChoice(value:PromptChoice | string){
75
+ if(!this.promptChoices || !Array.isArray(this.promptChoices)) this.promptChoices = []
76
+ this.promptChoices!.push(typeof(value)=='string' ? {title:value,value} : value)
77
+ this.resetChoices()
78
+ }
79
+ removeChoice(value:any){
80
+ this.promptChoices =this.promptChoices?.filter(choice=>choice.value!==value)
81
+ this.resetChoices()
82
+ }
83
+ clearChoice(){
84
+ this.promptChoices = []
85
+ this.resetChoices()
86
+ }
87
+
88
+
89
+ /**
90
+ * 返回选项的提示对象
91
+ *
92
+ * @remarks
93
+ *
94
+ *
95
+ *
96
+ * @param inputValue
97
+ * @returns
98
+ */
99
+ getPrompt(inputValue?:any): PromptObject | undefined {
100
+ return this.prompt?.get(inputValue)
101
+ }
102
+ }