mixcli 3.0.0 → 3.0.2

Sign up to get free protection for your applications and to get access to all the features.
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
+ }