mixcli 3.0.0 → 3.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,148 @@
1
+ // fork from https://github.com/sindresorhus/os-locale
2
+ const lcid = require('lcid')
3
+ const {exec, execSync} = require('./utils.js')
4
+ const fs = require('node:fs')
5
+ const path = require('node:path')
6
+ const os = require('node:os')
7
+ const defaultOptions = {spawn: true};
8
+ const defaultLocale = 'en-US';
9
+
10
+ async function getStdOut(command, args) {
11
+ return (await exec(command, args)).stdout;
12
+ }
13
+
14
+ function getStdOutSync(command, args) {
15
+ return execSync(command, args);
16
+ }
17
+
18
+ function getEnvLocale(env = process.env) {
19
+ return env.LC_ALL || env.LC_MESSAGES || env.LANG || env.LANGUAGE;
20
+ }
21
+
22
+ function parseLocale(string) {
23
+ const env = {};
24
+ for (const definition of string.split('\n')) {
25
+ const [key, value] = definition.split('=');
26
+ env[key] = value.replace(/^"|"$/g, '');
27
+ }
28
+
29
+ return getEnvLocale(env);
30
+ }
31
+
32
+ function getLocale(string) {
33
+ return (string && string.replace(/[.:].*/, ''));
34
+ }
35
+
36
+ async function getLocales() {
37
+ return getStdOut('locale', ['-a']);
38
+ }
39
+
40
+ function getLocalesSync() {
41
+ return getStdOutSync('locale', ['-a']);
42
+ }
43
+
44
+ function getSupportedLocale(locale, locales = '') {
45
+ return locales.includes(locale) ? locale : defaultLocale;
46
+ }
47
+
48
+ async function getAppleLocale() {
49
+ const results = await Promise.all([
50
+ getStdOut('defaults', ['read', '-globalDomain', 'AppleLocale']),
51
+ getLocales(),
52
+ ]);
53
+
54
+ return getSupportedLocale(results[0], results[1]);
55
+ }
56
+
57
+ function getAppleLocaleSync() {
58
+ return getSupportedLocale(
59
+ getStdOutSync('defaults', ['read', '-globalDomain', 'AppleLocale']),
60
+ getLocalesSync(),
61
+ );
62
+ }
63
+
64
+ async function getUnixLocale() {
65
+ return getLocale(parseLocale(await getStdOut('locale')));
66
+ }
67
+
68
+ function getUnixLocaleSync() {
69
+ return getLocale(parseLocale(getStdOutSync('locale')));
70
+ }
71
+
72
+ async function getWinLocale() {
73
+ const stdout = await getStdOut('wmic', ['os', 'get', 'locale']);
74
+ const lcidCode = Number.parseInt(stdout.replace('Locale', ''), 16);
75
+
76
+ return lcid.from(lcidCode);
77
+ }
78
+
79
+ function getWinLocaleSync() {
80
+ const stdout = getStdOutSync('wmic', ['os', 'get', 'locale']);
81
+ const lcidCode = Number.parseInt(stdout.replace('Locale', ''), 16);
82
+
83
+ return lcid.from(lcidCode);
84
+ }
85
+
86
+ function normalise(input) {
87
+ return input.replace(/_/, '-');
88
+ }
89
+
90
+ const cache = new Map();
91
+
92
+
93
+ function osLocaleSync(options = defaultOptions) {
94
+ if (cache.has(options.spawn)) {
95
+ return cache.get(options.spawn);
96
+ }
97
+
98
+ let locale;
99
+ try {
100
+ const envLocale = getEnvLocale();
101
+
102
+ if (envLocale || options.spawn === false) {
103
+ locale = getLocale(envLocale);
104
+ } else if (process.platform === 'win32') {
105
+ locale = getWinLocaleSync();
106
+ } else if (process.platform === 'darwin') {
107
+ locale = getAppleLocaleSync();
108
+ } else {
109
+ locale = getUnixLocaleSync();
110
+ }
111
+ } catch {}
112
+
113
+ const normalised = normalise(locale || defaultLocale);
114
+ cache.set(options.spawn, normalised);
115
+ return normalised;
116
+ }
117
+
118
+
119
+ function getCliLanguage(){
120
+ const supportedLanguages = require("./languages/settings.json").languages
121
+ const savedLangFile = path.join(os.tmpdir(),"voerkai18n_cli_language")
122
+ let result
123
+ const envLang = process.env.LANGUAGE;
124
+ if(envLang){
125
+ result = envLang.trim()
126
+ }else{
127
+ if(fs.existsSync(savedLangFile)){
128
+ try{
129
+ result = fs.readFileSync(savedLangFile).trim()
130
+ }catch{}
131
+ }
132
+ if(!result || !supportedLanguages.some(lng=>lng.name==result) ){
133
+ result = osLocaleSync()
134
+ result = result.split("-")[0]
135
+ }
136
+ }
137
+ if(!supportedLanguages.some(lng=>lng.name==result) ){
138
+ result = "en"
139
+ }
140
+ // 记住上次使用的语言
141
+ fs.writeFileSync(savedLangFile,result)
142
+ return result
143
+ }
144
+
145
+
146
+ module.exports = {
147
+ getCliLanguage
148
+ }
package/src/prompt.ts ADDED
@@ -0,0 +1,180 @@
1
+ import { PromptObject } from "prompts"
2
+ import { outputDebug } from "./utils"
3
+
4
+
5
+ export type PromptType = "text" | "password" | "invisible" | "number"| "confirm"| "list"| "toggle"| "select" | "multiselect" | "autocomplete" | "date" | "autocompleteMultiselect"
6
+
7
+ export type PromptParam = 'auto' | boolean | PromptType | PromptObject
8
+ export type InputPromptParam = PromptParam | ((value:any)=>PromptParam) | boolean
9
+ export type PromptParamDefaultValue = string | boolean | string[]
10
+
11
+ export const promptTypeMap:Record<string,string> = {
12
+ boolean:"confirm",
13
+ string:"text",
14
+ number:"number",
15
+ array:"list",
16
+ }
17
+
18
+ export const supportedPromptTypes = ["text","password","invisible", "number", "confirm" , "list", "toggle" , "select" , "multiselect" , "autocomplete" , "date" , "autocompleteMultiselect"]
19
+ export interface PromptChoice {
20
+ title: string;
21
+ value?: any;
22
+ disabled?: boolean | undefined;
23
+ selected?: boolean | undefined;
24
+ description?: string | undefined;
25
+ }
26
+
27
+
28
+
29
+ export interface IPromptableOptions{
30
+ required?: boolean; // A value must be supplied when the option is specified.
31
+ optional?: boolean; // A value is optional when the option is specified.
32
+ default?:PromptParamDefaultValue
33
+ choices?:(PromptChoice | any)[] // 选项值的可选值
34
+ prompt?:InputPromptParam
35
+ validate?:(value: any) => boolean
36
+ }
37
+
38
+
39
+ export interface IPromptable{
40
+ name():string
41
+ description?:string
42
+ flags:string
43
+ promptChoices?:PromptChoice[]
44
+ argChoices?:string[]
45
+ variadic?:boolean
46
+ defaultValue?:PromptParamDefaultValue
47
+ input?:any
48
+ required?:boolean
49
+ validate?: (value: any) => boolean
50
+ getPrompt(inputValue?:any):PromptObject | undefined
51
+ }
52
+
53
+ /**
54
+ * 供command.option()使用的参数对象
55
+ */
56
+ export interface PromptableObject{
57
+
58
+
59
+ }
60
+
61
+
62
+ /**
63
+ * 负责生成prompt对象
64
+ *
65
+ */
66
+ export class PromptManager{
67
+ args:InputPromptParam
68
+ private _promptable:IPromptable // 对应的FlexOption或FlexArgument
69
+ constructor(promptable:IPromptable,promptArgs?:InputPromptParam){
70
+ this._promptable = promptable
71
+ this.args= promptArgs===undefined ? 'auto' : promptArgs
72
+ }
73
+
74
+ /**
75
+ * 返回输入的是否是有效的prompt类型
76
+ * @param type
77
+ * @returns
78
+ */
79
+ isValid(type:any){
80
+ return supportedPromptTypes.includes(String(type))
81
+ }
82
+ /**
83
+ * 推断是否需要提示
84
+ *
85
+ */
86
+ isNeed(input:any,defaultValue?:any){
87
+
88
+ const promptArg = this.args
89
+ const inputValue = input || defaultValue
90
+ // 是否有输入值,即在命令行输入了值
91
+ const hasInput = !(inputValue === undefined)
92
+ // 1. 显式指定了_prompt为true,则需要提示,后续进行提示类型的推断,可能不会准确
93
+ if(promptArg===true) return true
94
+ if(promptArg===false) return false
95
+
96
+ // 2. 提供了一个prompt对象,并且没有在命令行输入值,则需要提示
97
+ if(typeof(promptArg)=='object'){
98
+ return !hasInput
99
+ }
100
+
101
+ // 3. 指定了内置的prompt类型,如prompt='password',则使用password类型提示输入
102
+ if(typeof(promptArg) == 'string' && supportedPromptTypes.includes(promptArg)){
103
+ return !hasInput
104
+ }
105
+
106
+ // 4. 判断输入是否有效,则显示提示
107
+ if(this._promptable.argChoices && this._promptable.argChoices.indexOf(inputValue) == -1){
108
+ return true
109
+ }
110
+ return !hasInput
111
+ }
112
+ /**
113
+ * 返回生成prompt对象
114
+ *
115
+ * @param inputValue 从命令行输入的值
116
+ */
117
+ get(inputValue?:any){
118
+ const {description,promptChoices,validate,defaultValue} = this._promptable
119
+ let input = inputValue || defaultValue
120
+ // 判断是否需要输入提示
121
+ if(!this.isNeed(input,defaultValue)) return
122
+ // 推断prompt类型
123
+ let promptType = this.infer(inputValue)
124
+ const prompt = {
125
+ type:promptType,
126
+ name:this._promptable.name(),
127
+ message:description,
128
+ initial: input,
129
+ ...typeof(this.args) == 'object' ? this.args : {}
130
+ } as PromptObject
131
+ // 指定了验证函数,用来验证输入值是否有效
132
+ prompt.validate = validate?.bind(this._promptable)
133
+ if(promptType=='multiselect') prompt.instructions=false
134
+ if(['select','multiselect'].includes(promptType)){
135
+ let index = promptChoices?.findIndex(item=>item.value==input)
136
+ prompt.initial = index==-1 ? undefined : index
137
+ }
138
+ // 选项值的可选值
139
+ if(Array.isArray(promptChoices)) {
140
+ prompt.choices =promptChoices
141
+ }
142
+ return prompt
143
+ }
144
+ /**
145
+ * 推断prompt类型
146
+ *
147
+ * @param inputValue 从命令行输入的值
148
+ */
149
+ infer(inputValue?:any){
150
+ const {argChoices,variadic,defaultValue} = this._promptable
151
+ let input = inputValue || defaultValue
152
+ // 如果选择指定了"-p [value]或[value...]",则使用text类型,如果没有要求输入值,则使用confirm类型
153
+ let promptType = /(\<[\w\.]+\>)|(\[[\w\.]+\])/.test(this._promptable.flags) ? 'text' : 'confirm'
154
+ let promptArg = this.args
155
+ if(this.isValid(promptArg)){ // 显式指定了prompt类型
156
+ promptType = promptArg as string
157
+ }else{ // 未显式指定prompt类型,需要按一定规则推断类型
158
+ if(typeof(promptArg)=='object'){
159
+ promptType = promptArg.type as string
160
+ }else{
161
+ if(argChoices){ // 提供多个可选值时
162
+ promptType = variadic ? 'multiselect' : 'select'
163
+ }else{
164
+ const datatype:string = Array.isArray(defaultValue) ? 'array' : typeof(defaultValue)
165
+ // 如果输入值班是数组,则使用list类型,允许使用逗号分隔的多个值
166
+ if(Array.isArray(input) || variadic){
167
+ promptType = "list"
168
+ }else{
169
+ if(datatype in promptTypeMap){
170
+ promptType = promptTypeMap[datatype]
171
+ }
172
+ }
173
+ }
174
+ }
175
+ }
176
+ outputDebug("选项<{}> -> 提示类型<{}>",[this._promptable.name(),promptType])
177
+ return promptType
178
+ }
179
+
180
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,128 @@
1
+ import artTemplate from "art-template"
2
+ import fs from "fs-extra"
3
+ import path from "node:path"
4
+ import { promisify } from "flex-tools/func/promisify"
5
+ import logsets from "logsets"
6
+ /**
7
+ *
8
+ * 在控制台输出一个字符串
9
+ * 本方法会将字符串中的每一行空格去掉
10
+ *
11
+ * @remarks
12
+ *
13
+ * outputStr(String.raw`
14
+ * a
15
+ * b`)
16
+ *
17
+ * 会输出
18
+ * a
19
+ * b
20
+ *
21
+ * 此功能可以用于输出多行字符串时,保持代码的缩进格式,而不会影响输出结果
22
+ *
23
+ * @param str : 要输出的字符串
24
+ * @param vars : 用于替换字符串中的变量
25
+ *
26
+ */
27
+ export function outputStr(str:string,vars?:Record<string,any> | any[]){
28
+ logsets.log(fixIndent(str),vars)
29
+ }
30
+
31
+ /**
32
+ * 修正多行字符串的缩进
33
+ *
34
+ * @param text
35
+ * @param indent
36
+ * @returns
37
+ */
38
+ export function fixIndent(text:string,indent?:boolean | number):string{
39
+ let indentValue = (indent==undefined || indent===true) ? 0 : (typeof(indent)=='number' ? indent : -1)
40
+ if(indentValue==-1) return text // 不修正缩进
41
+ let lines:string[] = text.split("\n")
42
+ let minSpaceCount = lines.reduce<number>((minCount,line,index)=>{
43
+ if(index==0) return minCount
44
+ const spaceCount = line.match(/^\s*/)?.[0].length || 0
45
+ return Math.min(minCount,spaceCount)
46
+ },9999)
47
+ lines = lines.map(line=>line.substring(minSpaceCount))
48
+ return lines.join("\n")
49
+ }
50
+
51
+ /**
52
+ * 增加内置选项
53
+ * @param command
54
+ */
55
+ export function addBuiltInOptions(command:any){
56
+ command.option("--work-dirs <values...>","指定工作目录",{hidden:true,optional:true,required:true,prompt:false})
57
+ command.option("--disable-prompts","禁用所有交互提示",{hidden:true,prompt:false})
58
+ command.option("--debug-cli","显示调试信息",{hidden:true,prompt:false})
59
+ }
60
+
61
+
62
+ /**
63
+ * 是否命令行中包含了--debug-cli选项
64
+ */
65
+ export function isDebug(){
66
+ return process.argv.includes("--debug-cli")
67
+ }
68
+ export function isEnablePrompts(){
69
+ return !process.argv.includes("--disable-prompts")
70
+ }
71
+
72
+ /**
73
+ * 打印调试信息
74
+ * @param message
75
+ * @param args
76
+ */
77
+ export function outputDebug(message:string,...args:any[]){
78
+ let vars = (args.length == 1 && typeof(args[0])=='function') ? args[0]() : args
79
+ if(isDebug()) logsets.log(`[MixCli] ${message}`,...vars)
80
+ }
81
+
82
+ export const fileExists = promisify(fs.exists,{
83
+ parseCallback:(results)=>{
84
+ return results[0]
85
+ }
86
+ })
87
+ export const readFile = promisify(fs.readFile)
88
+ export const writeFile = promisify(fs.writeFile)
89
+ export const mkdir = promisify(fs.mkdir)
90
+
91
+ /**
92
+ * 基于artTemplate模板生成文件
93
+ *
94
+ * @param {*} tmplFile
95
+ * @param {*} vars
96
+ */
97
+ export async function createFileByTemplate(targetFile:string,tmplFile:string,vars:Record<string,any>={}){
98
+ tmplFile=path.isAbsolute(tmplFile)? tmplFile : path.join(process.cwd(),tmplFile)
99
+ if(!fs.existsSync(tmplFile)){
100
+ throw new Error("模板文件不存在:"+tmplFile)
101
+ }
102
+ targetFile=path.isAbsolute(targetFile)? targetFile : path.join(process.cwd(),targetFile)
103
+ const outPath = path.dirname(targetFile)
104
+ if(!await fileExists(outPath)){
105
+ await mkdir(outPath,{recursive:true})
106
+ }
107
+ const template = artTemplate(tmplFile,await readFile(tmplFile,{encoding:"utf-8"}));
108
+ await writeFile(targetFile,template(vars),{encoding:"utf-8"})
109
+ return targetFile
110
+ }
111
+
112
+ /**
113
+ * 创建目录
114
+ *
115
+ *
116
+ *
117
+ * @param {String[]} dirs 要创建的目录列表,类型为字符串数组
118
+ * @param callback 创建目录过程中的回调函数,类型为异步函数,接收一个参数 dir,表示当前正在创建的目录
119
+ * @returns 该函数返回一个 Promise 对象,表示创建目录的操作是否完成
120
+ */
121
+ export async function mkDirs(dirs:string[],{callback,base}:{callback?:Function,base?:string}){
122
+ if(!Array.isArray(dirs)) throw new Error("dirs参数必须为字符串数组")
123
+ for(let dir of dirs){
124
+ if(!path.isAbsolute(dir)) dir = path.join(base || process.cwd(),dir)
125
+ if(typeof(callback)=='function') callback(dir)
126
+ await mkdir(dir,{recursive:true})
127
+ }
128
+ }