feat: migrate from inquirer to @inquirer/prompts for interactive prompts

- Replaced inquirer with @inquirer/prompts in various command files for improved prompt handling.
- Updated confirmation and input prompts in commands such as app, config, deploy, login, and others.
- Added a new command 'cc' for switching between Claude code models with appropriate configurations.
- Enhanced user experience by ensuring prompts are more streamlined and consistent across the application.
This commit is contained in:
2026-01-15 09:17:07 +08:00
parent 4f541748da
commit 8549a4aa53
15 changed files with 585 additions and 309 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "@kevisual/cli", "name": "@kevisual/cli",
"version": "0.0.78", "version": "0.0.80",
"description": "envision 命令行工具", "description": "envision 命令行工具",
"type": "module", "type": "module",
"basename": "/root/cli", "basename": "/root/cli",
@@ -41,9 +41,11 @@
], ],
"author": "abearxiong", "author": "abearxiong",
"dependencies": { "dependencies": {
"@inquirer/prompts": "^8.2.0",
"@kevisual/app": "^0.0.2", "@kevisual/app": "^0.0.2",
"@kevisual/context": "^0.0.4", "@kevisual/context": "^0.0.4",
"@kevisual/hot-api": "^0.0.3", "@kevisual/hot-api": "^0.0.3",
"@kevisual/use-config": "^1.0.26",
"@nut-tree-fork/nut-js": "^4.2.6", "@nut-tree-fork/nut-js": "^4.2.6",
"eventemitter3": "^5.0.1", "eventemitter3": "^5.0.1",
"lowdb": "^7.0.1", "lowdb": "^7.0.1",
@@ -57,13 +59,13 @@
"@kevisual/dts": "^0.0.3", "@kevisual/dts": "^0.0.3",
"@kevisual/load": "^0.0.6", "@kevisual/load": "^0.0.6",
"@kevisual/logger": "^0.0.4", "@kevisual/logger": "^0.0.4",
"@kevisual/query": "0.0.33", "@kevisual/query": "0.0.35",
"@kevisual/query-login": "0.0.7", "@kevisual/query-login": "0.0.7",
"@types/bun": "^1.3.5", "@types/bun": "^1.3.6",
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/jsonwebtoken": "^9.0.10", "@types/jsonwebtoken": "^9.0.10",
"@types/micromatch": "^4.0.10", "@types/micromatch": "^4.0.10",
"@types/node": "^25.0.3", "@types/node": "^25.0.8",
"@types/semver": "^7.7.1", "@types/semver": "^7.7.1",
"chalk": "^5.6.2", "chalk": "^5.6.2",
"commander": "^14.0.2", "commander": "^14.0.2",
@@ -72,10 +74,9 @@
"filesize": "^11.0.13", "filesize": "^11.0.13",
"form-data": "^4.0.5", "form-data": "^4.0.5",
"ignore": "^7.0.5", "ignore": "^7.0.5",
"inquirer": "^13.1.0",
"jsonwebtoken": "^9.0.3", "jsonwebtoken": "^9.0.3",
"tar": "^7.5.2", "tar": "^7.5.2",
"zustand": "^5.0.9" "zustand": "^5.0.10"
}, },
"engines": { "engines": {
"node": ">=22.0.0" "node": ">=22.0.0"

521
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,7 @@ import { fileIsExist } from '@/uitls/file.ts';
import fs from 'fs'; import fs from 'fs';
import { getConfig } from '@/module/get-config.ts'; import { getConfig } from '@/module/get-config.ts';
import path from 'path'; import path from 'path';
import inquirer from 'inquirer'; import { confirm } from '@inquirer/prompts';
import { baseURL, getUrl } from '@/module/query.ts'; import { baseURL, getUrl } from '@/module/query.ts';
export const appCommand = new Command('app').description('app 命令').action(() => { export const appCommand = new Command('app').description('app 命令').action(() => {
console.log('app'); console.log('app');
@@ -99,15 +99,11 @@ const uninstallAppCommand = new Command('uninstall')
if (!checkPath) { if (!checkPath) {
console.error(chalk.red('path is error, 请输入正确的路径')); console.error(chalk.red('path is error, 请输入正确的路径'));
} else { } else {
const answer = await inquirer.prompt([ const confirmed = await confirm({
{
type: 'confirm',
name: 'confirm',
message: `确定要删除 ${_path} 吗?`, message: `确定要删除 ${_path} 吗?`,
default: false, default: false,
}, });
]); if (confirmed) {
if (answer.confirm) {
fs.rmSync(_path, { recursive: true }); fs.rmSync(_path, { recursive: true });
console.log(chalk.green('删除成功', _path)); console.log(chalk.green('删除成功', _path));
} }

120
src/command/cc.ts Normal file
View File

@@ -0,0 +1,120 @@
import { program, Command } from '@/program.ts';
import { chalk } from '../module/chalk.ts';
import path from 'node:path';
import { spawn } from 'node:child_process';
import { useKey } from '@kevisual/use-config';
import os from 'node:os'
import fs from 'node:fs';
import { select } from '@inquirer/prompts';
const MODELS = ['minimax', 'glm'] as const;
type Model = typeof MODELS[number];
const changeMinimax = (token?: string) => {
const auth_token = token || useKey("MINIMAX_API_KEY")
return {
"env": {
"ANTHROPIC_AUTH_TOKEN": auth_token,
"ANTHROPIC_BASE_URL": "https://api.minimaxi.com/anthropic",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "MiniMax-M2.1",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "MiniMax-M2.1",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "MiniMax-M2.1",
"ANTHROPIC_MODEL": "MiniMax-M2.1",
"API_TIMEOUT_MS": "3000000",
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": 1
}
}
}
const changeGLM = (token?: string) => {
const auth_token = token || useKey('ZHIPU_API_KEY')
return {
"env": {
"ANTHROPIC_AUTH_TOKEN": auth_token,
"ANTHROPIC_BASE_URL": "https://open.bigmodel.cn/api/anthropic",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "glm-4.7",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "glm-4.7",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "glm-4.7",
"ANTHROPIC_MODEL": "glm-4.7"
}
}
}
/**
* 跳过登录检查,在~/.claude.json的配置中添加字段 "hasCompletedOnboarding": true
*/
const changeNoCheck = () => {
const homeDir = os.homedir();
const claudeConfigPath = path.join(homeDir, '.claude.json');
let claudeConfig = {};
if (fs.existsSync(claudeConfigPath)) {
const content = fs.readFileSync(claudeConfigPath, 'utf-8');
try {
claudeConfig = JSON.parse(content);
} catch {
claudeConfig = {};
}
}
claudeConfig = {
...claudeConfig,
hasCompletedOnboarding: true
};
fs.writeFileSync(claudeConfigPath, JSON.stringify(claudeConfig, null, 2));
}
const modelConfig: Record<Model, (token?: string) => object> = {
minimax: changeMinimax,
glm: changeGLM,
};
const readOrCreateConfig = (configPath: string): Record<string, unknown> => {
if (fs.existsSync(configPath)) {
const content = fs.readFileSync(configPath, 'utf-8');
try {
return JSON.parse(content);
} catch {
return {};
}
}
return {};
};
const saveConfig = (configPath: string, config: Record<string, unknown>) => {
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
};
export const command = new Command('cc')
.description('切换claude code模型支持GLM4.7和Minimax')
.option('-m, --models <model:string>', `选择模型: ${MODELS.join(' | ')}`)
.action(async (options) => {
const configPath = path.join(os.homedir(), '.claude/settings.json');
// 读取或创建配置
const config = readOrCreateConfig(configPath);
// 如果没有指定模型,使用 inquire 选择
let selectedModel: Model;
if (options.models && MODELS.includes(options.models as Model)) {
selectedModel = options.models as Model;
} else {
selectedModel = await select({
message: '请选择模型:',
choices: MODELS,
});
}
// 更新配置
const updateConfig = modelConfig[selectedModel]();
Object.assign(config, updateConfig);
// 保存配置
saveConfig(configPath, config);
changeNoCheck();
console.log(`已切换到模型: ${chalk.green(selectedModel)}`);
console.log(`配置已保存到: ${configPath}`);
});
program.addCommand(command)

View File

@@ -3,7 +3,7 @@ import { checkFileExists, getConfig, writeConfig } from '@/module/index.ts';
import path from 'path'; import path from 'path';
import fs from 'fs'; import fs from 'fs';
import { chalk } from '@/module/chalk.ts'; import { chalk } from '@/module/chalk.ts';
import inquirer from 'inquirer'; import { confirm, input } from '@inquirer/prompts';
// 设置工作目录 // 设置工作目录
const setWorkdir = async (options: { workdir: string }) => { const setWorkdir = async (options: { workdir: string }) => {
@@ -24,15 +24,11 @@ const setWorkdir = async (options: { workdir: string }) => {
console.log('路径不存在'); console.log('路径不存在');
fs.mkdirSync(finalPath, { recursive: true }); fs.mkdirSync(finalPath, { recursive: true });
} }
const answers = await inquirer.prompt([ const confirmed = await confirm({
{
type: 'confirm',
name: 'confirm',
message: `Are you sure you want to set the workdir to: ${finalPath}?`, message: `Are you sure you want to set the workdir to: ${finalPath}?`,
default: false, default: false,
}, });
]); if (confirmed) {
if (answers.confirm) {
flag = true; flag = true;
config.workdir = finalPath; config.workdir = finalPath;
console.log(chalk.green(`set workdir success:`, finalPath)); console.log(chalk.green(`set workdir success:`, finalPath));
@@ -81,15 +77,11 @@ const command = new Command('config')
console.log('路径不存在'); console.log('路径不存在');
fs.mkdirSync(finalPath, { recursive: true }); fs.mkdirSync(finalPath, { recursive: true });
} }
const answers = await inquirer.prompt([ const confirmed = await confirm({
{
type: 'confirm',
name: 'confirm',
message: `Are you sure you want to set the workdir to: ${finalPath}?`, message: `Are you sure you want to set the workdir to: ${finalPath}?`,
default: false, default: false,
}, });
]); if (confirmed) {
if (answers.confirm) {
flag = true; flag = true;
config.workdir = finalPath; config.workdir = finalPath;
console.log(chalk.green(`set workdir success:`, finalPath)); console.log(chalk.green(`set workdir success:`, finalPath));
@@ -100,15 +92,11 @@ const command = new Command('config')
if (options.set) { if (options.set) {
const key = options.set; const key = options.set;
let value = options.value; let value = options.value;
const answer = await inquirer.prompt([ if (!value) {
{ value = await input({
type: 'input',
name: 'value',
message: `Enter your ${key}:(current: ${config[key]})`, message: `Enter your ${key}:(current: ${config[key]})`,
when: () => !value, });
}, }
]);
value = answer.value || value;
if (key && value) { if (key && value) {
flag = true; flag = true;
config[key] = value; config[key] = value;
@@ -138,16 +126,10 @@ const setCommand = new Command('set')
return; return;
} }
let flag = false; let flag = false;
const answer = await inquirer.prompt([ if (value === 'not_input') {
{ value = await input({
type: 'input',
name: 'value',
message: `Enter your ${key}:(current: ${config[key]})`, message: `Enter your ${key}:(current: ${config[key]})`,
when: () => value === 'not_input', });
},
]);
if (value === 'not_input' && !answer.value) {
value = '';
} }
if (key === 'workdir') { if (key === 'workdir') {
await setWorkdir({ workdir: value }); await setWorkdir({ workdir: value });
@@ -192,15 +174,11 @@ const getCommand = new Command('get')
.action(async (key) => { .action(async (key) => {
const config = getConfig(); const config = getConfig();
const keys = Object.keys(config); const keys = Object.keys(config);
const answer = await inquirer.prompt([ if (!key) {
{ key = await input({
type: 'input',
name: 'key',
message: `Enter your key:(keys: ${JSON.stringify(keys)})`, message: `Enter your key:(keys: ${JSON.stringify(keys)})`,
when: () => !key, });
}, }
]);
key = answer.key || key;
if (config[key]) { if (config[key]) {
console.log(chalk.green(`get ${key}:`)); console.log(chalk.green(`get ${key}:`));

View File

@@ -4,7 +4,7 @@ import path from 'path';
import fs from 'fs'; import fs from 'fs';
import FormData from 'form-data'; import FormData from 'form-data';
import { getBaseURL, query, storage } from '@/module/query.ts'; import { getBaseURL, query, storage } from '@/module/query.ts';
import inquirer from 'inquirer'; import { input, confirm } from '@inquirer/prompts';
import chalk from 'chalk'; import chalk from 'chalk';
import { upload } from '@/module/download/upload.ts'; import { upload } from '@/module/download/upload.ts';
import { getHash } from '@/uitls/hash.ts'; import { getHash } from '@/uitls/hash.ts';
@@ -60,23 +60,15 @@ const command = new Command('deploy')
key = pkgInfo?.appKey || ''; key = pkgInfo?.appKey || '';
} }
logger.debug('start deploy'); logger.debug('start deploy');
if (!version || !key) { if (!version) {
const answers = await inquirer.prompt([ version = await input({
{
type: 'input',
name: 'version',
message: 'Enter your version:', message: 'Enter your version:',
when: () => !version, });
}, }
{ if (!key) {
type: 'input', key = await input({
name: 'key',
message: 'Enter your key:', message: 'Enter your key:',
when: () => !key, });
},
]);
version = answers.version || version;
key = answers.key || key;
} }
const pwd = process.cwd(); const pwd = process.cwd();
const directory = path.join(pwd, filePath); const directory = path.join(pwd, filePath);
@@ -110,14 +102,10 @@ const command = new Command('deploy')
logger.debug('upload Files Key', key, version); logger.debug('upload Files Key', key, version);
if (!yes) { if (!yes) {
// 确认是否上传 // 确认是否上传
const confirm = await inquirer.prompt([ const confirmed = await confirm({
{
type: 'confirm',
name: 'confirm',
message: 'Do you want to upload these files?', message: 'Do you want to upload these files?',
}, });
]); if (!confirmed) {
if (!confirm.confirm) {
return; return;
} }
} }

View File

@@ -1,6 +1,6 @@
import { program, Command } from '@/program.ts'; import { program, Command } from '@/program.ts';
import { getConfig, getEnvToken } from '@/module/get-config.ts'; import { getConfig, getEnvToken } from '@/module/get-config.ts';
import inquirer from 'inquirer'; import { input, password } from '@inquirer/prompts';
import { loginInCommand } from '@/module/login/login-by-web.ts'; import { loginInCommand } from '@/module/login/login-by-web.ts';
import { queryLogin, storage } from '@/module/query.ts'; import { queryLogin, storage } from '@/module/query.ts';
import chalk from 'chalk'; import chalk from 'chalk';
@@ -28,24 +28,15 @@ const loginCommand = new Command('login')
return; return;
} }
// 如果没有传递参数,则通过交互式输入 // 如果没有传递参数,则通过交互式输入
if (!username || !password) { if (!username) {
const answers = await inquirer.prompt([ username = await input({
{
type: 'input',
name: 'username',
message: 'Enter your username:', message: 'Enter your username:',
when: () => !username, // 当 username 为空时,提示用户输入 });
}, }
{ if (!password) {
type: 'password', password = await password({
name: 'password',
message: 'Enter your password:', message: 'Enter your password:',
mask: '*', // 隐藏输入的字符 });
when: () => !password, // 当 password 为空时,提示用户输入
},
]);
username = answers.username || username;
password = answers.password || password;
} }
const token = storage.getItem('token'); const token = storage.getItem('token');
if (token) { if (token) {

View File

@@ -1,7 +1,7 @@
import { program as app, Command } from '@/program.ts'; import { program as app, Command } from '@/program.ts';
import { getConfig, getEnvToken, writeConfig } from '@/module/index.ts'; import { getConfig, getEnvToken, writeConfig } from '@/module/index.ts';
import { queryLogin, storage } from '@/module/query.ts'; import { queryLogin, storage } from '@/module/query.ts';
import inquirer from 'inquirer'; import { input } from '@inquirer/prompts';
import util from 'util'; import util from 'util';
function isNumeric(str: string) { function isNumeric(str: string) {
return /^-?\d+\.?\d*$/.test(str); return /^-?\d+\.?\d*$/.test(str);
@@ -136,14 +136,9 @@ const setBaseURL = new Command('set')
const config = getConfig(); const config = getConfig();
let baseURL = opt.baseURL; let baseURL = opt.baseURL;
if (!baseURL) { if (!baseURL) {
const answers = await inquirer.prompt([ baseURL = await input({
{
type: 'input',
name: 'baseURL',
message: `Enter your baseURL:(current: ${config.baseURL})`, message: `Enter your baseURL:(current: ${config.baseURL})`,
}, });
]);
baseURL = answers.baseURL;
if (!baseURL) { if (!baseURL) {
console.log('baseURL is required'); console.log('baseURL is required');
return; return;

View File

@@ -5,7 +5,7 @@ import { spawn } from 'child_process';
import { fileIsExist } from '@/uitls/file.ts'; import { fileIsExist } from '@/uitls/file.ts';
import { getConfig } from '@/module/get-config.ts'; import { getConfig } from '@/module/get-config.ts';
import fs from 'fs'; import fs from 'fs';
import inquirer from 'inquirer'; import { select, confirm } from '@inquirer/prompts';
import { checkPnpm } from '@/uitls/npm.ts'; import { checkPnpm } from '@/uitls/npm.ts';
const parseIfJson = (str: string) => { const parseIfJson = (str: string) => {
try { try {
@@ -21,10 +21,8 @@ const publish = new Command('publish')
.option('-t, --tag', 'tag') .option('-t, --tag', 'tag')
.description('publish npm') .description('publish npm')
.action(async (registry, options) => { .action(async (registry, options) => {
const answer = await inquirer.prompt([ if (!registry) {
{ registry = await select({
type: 'list',
name: 'publish',
message: 'Select the registry to publish', message: 'Select the registry to publish',
choices: [ choices: [
{ {
@@ -36,10 +34,8 @@ const publish = new Command('publish')
value: 'npm', value: 'npm',
}, },
], ],
when: !registry, });
}, }
]);
registry = registry || answer.publish;
const config = getConfig(); const config = getConfig();
let cmd = ''; let cmd = '';
const execPath = process.cwd(); const execPath = process.cwd();
@@ -149,15 +145,11 @@ const npmrc = new Command('set')
if (options.force) { if (options.force) {
writeFlag = true; writeFlag = true;
} else { } else {
const answer = await inquirer.prompt([ const confirmed = await confirm({
{
type: 'confirm',
name: 'confirm',
message: `Are you sure you want to overwrite the .npmrc file?`, message: `Are you sure you want to overwrite the .npmrc file?`,
default: false, default: false,
}, });
]); if (confirmed) {
if (answer.confirm) {
writeFlag = true; writeFlag = true;
} }
} }

View File

@@ -1,6 +1,5 @@
import { program, Command } from '@/program.ts'; import { program, Command } from '@/program.ts';
import { chalk } from '@/module/chalk.ts'; import { chalk } from '@/module/chalk.ts';
import inquirer from 'inquirer';
const command = new Command('proxy') const command = new Command('proxy')
.description('执行代理相关的命令') .description('执行代理相关的命令')

View File

@@ -6,7 +6,7 @@ import { getConfig, query } from '@/module/index.ts';
import { fileIsExist } from '@/uitls/file.ts'; import { fileIsExist } from '@/uitls/file.ts';
import { chalk } from '@/module/chalk.ts'; import { chalk } from '@/module/chalk.ts';
import * as backServices from '@/query/services/index.ts'; import * as backServices from '@/query/services/index.ts';
import inquirer from 'inquirer'; import { select, input } from '@inquirer/prompts';
import { logger } from '@/module/logger.ts'; import { logger } from '@/module/logger.ts';
// 查找文件(忽略大小写) // 查找文件(忽略大小写)
async function findFileInsensitive(targetFile: string): Promise<string | null> { async function findFileInsensitive(targetFile: string): Promise<string | null> {

View File

@@ -1,5 +1,5 @@
import { program, Command } from '@/program.ts'; import { program, Command } from '@/program.ts';
import inquirer from 'inquirer'; import { input } from '@inquirer/prompts';
import { query } from '../module/index.ts'; import { query } from '../module/index.ts';
import chalk from 'chalk'; import chalk from 'chalk';
import util from 'util'; import util from 'util';
@@ -15,27 +15,14 @@ const command = new Command('service')
let { path, key } = options; let { path, key } = options;
// 如果没有传递参数,则通过交互式输入 // 如果没有传递参数,则通过交互式输入
if (!path) { if (!path) {
const answers = await inquirer.prompt([ path = await input({
{
type: 'input',
name: 'path',
required: true,
message: 'Enter your path:', message: 'Enter your path:',
when: () => !path, // 当 username 为空时,提示用户输入 });
},
]);
path = answers.path || path;
} }
if (!key) { if (!key) {
const answers = await inquirer.prompt([ key = await input({
{
type: 'input',
required: false,
name: 'key',
message: 'Enter your key:', message: 'Enter your key:',
}, });
]);
key = answers.key || key;
} }
const res = await query.post({ path, key }); const res = await query.post({ path, key });
if (res?.code === 200) { if (res?.code === 200) {

View File

@@ -17,6 +17,7 @@ import './command/gist/index.ts';
import './command/config-remote.ts'; import './command/config-remote.ts';
import './command/config-secret-remote.ts'; import './command/config-secret-remote.ts';
import './command/ai.ts'; import './command/ai.ts';
import './command/cc.ts'
// program.parse(process.argv); // program.parse(process.argv);
export const runParser = async (argv: string[]) => { export const runParser = async (argv: string[]) => {

View File

@@ -5,7 +5,7 @@ import { chalk } from '../chalk.ts';
import { Result } from '@kevisual/query'; import { Result } from '@kevisual/query';
import { fileIsExist } from '@/uitls/file.ts'; import { fileIsExist } from '@/uitls/file.ts';
import { glob } from 'fast-glob'; import { glob } from 'fast-glob';
import inquirer from 'inquirer'; import { confirm } from '@inquirer/prompts';
import { getEnvToken } from '../get-config.ts'; import { getEnvToken } from '../get-config.ts';
type DownloadTask = { type DownloadTask = {
@@ -80,15 +80,11 @@ const checkDelete = async (opts?: { force?: boolean; dir?: string; yes?: boolean
try { try {
if (fileIsExist(dir)) { if (fileIsExist(dir)) {
const files = await glob(`${dir}/**/*`, { onlyFiles: true }); const files = await glob(`${dir}/**/*`, { onlyFiles: true });
const answers = await inquirer.prompt([ const shouldConfirm = files.length > 0 && !yes;
{ const confirmed = shouldConfirm ? await confirm({
type: 'confirm',
name: 'confirm',
message: `是否你需要删除 【${opts?.dir}】 目录下的文件. [${files.length}] 个?`, message: `是否你需要删除 【${opts?.dir}】 目录下的文件. [${files.length}] 个?`,
when: () => files.length > 0 && !yes, // 当 username 为空时,提示用户输入 }) : false;
}, if (confirmed || yes) {
]);
if (answers?.confirm || yes) {
fs.rmSync(dir, { recursive: true }); fs.rmSync(dir, { recursive: true });
console.log(chalk.green('删除成功', dir)); console.log(chalk.green('删除成功', dir));
} else { } else {

View File

@@ -1,6 +1,7 @@
import { program, Command } from 'commander'; import { program, Command } from 'commander';
import fs from 'fs'; import fs from 'fs';
import { useContextKey } from '@kevisual/context' import { useContextKey } from '@kevisual/context'
// 将多个子命令加入主程序中 // 将多个子命令加入主程序中
const version = useContextKey('version', () => { const version = useContextKey('version', () => {
let version = '0.0.64'; let version = '0.0.64';