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/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
|
+
}
|