mixcli 3.0.1 → 3.0.2
Sign up to get free protection for your applications and to get access to all the features.
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
- package/src/cli.ts +253 -0
- package/src/command.ts +451 -0
- package/src/finder.ts +142 -0
- package/src/index.ts +4 -0
- package/src/option.ts +102 -0
- package/src/oslocate.js +148 -0
- package/src/prompt.ts +180 -0
- package/src/utils.ts +128 -0
package/src/oslocate.js
ADDED
@@ -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
|
+
}
|