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.
@@ -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
+ }