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",
"version": "0.0.78",
"version": "0.0.80",
"description": "envision 命令行工具",
"type": "module",
"basename": "/root/cli",
@@ -41,9 +41,11 @@
],
"author": "abearxiong",
"dependencies": {
"@inquirer/prompts": "^8.2.0",
"@kevisual/app": "^0.0.2",
"@kevisual/context": "^0.0.4",
"@kevisual/hot-api": "^0.0.3",
"@kevisual/use-config": "^1.0.26",
"@nut-tree-fork/nut-js": "^4.2.6",
"eventemitter3": "^5.0.1",
"lowdb": "^7.0.1",
@@ -57,13 +59,13 @@
"@kevisual/dts": "^0.0.3",
"@kevisual/load": "^0.0.6",
"@kevisual/logger": "^0.0.4",
"@kevisual/query": "0.0.33",
"@kevisual/query": "0.0.35",
"@kevisual/query-login": "0.0.7",
"@types/bun": "^1.3.5",
"@types/bun": "^1.3.6",
"@types/crypto-js": "^4.2.2",
"@types/jsonwebtoken": "^9.0.10",
"@types/micromatch": "^4.0.10",
"@types/node": "^25.0.3",
"@types/node": "^25.0.8",
"@types/semver": "^7.7.1",
"chalk": "^5.6.2",
"commander": "^14.0.2",
@@ -72,10 +74,9 @@
"filesize": "^11.0.13",
"form-data": "^4.0.5",
"ignore": "^7.0.5",
"inquirer": "^13.1.0",
"jsonwebtoken": "^9.0.3",
"tar": "^7.5.2",
"zustand": "^5.0.9"
"zustand": "^5.0.10"
},
"engines": {
"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 { getConfig } from '@/module/get-config.ts';
import path from 'path';
import inquirer from 'inquirer';
import { confirm } from '@inquirer/prompts';
import { baseURL, getUrl } from '@/module/query.ts';
export const appCommand = new Command('app').description('app 命令').action(() => {
console.log('app');
@@ -99,15 +99,11 @@ const uninstallAppCommand = new Command('uninstall')
if (!checkPath) {
console.error(chalk.red('path is error, 请输入正确的路径'));
} else {
const answer = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
const confirmed = await confirm({
message: `确定要删除 ${_path} 吗?`,
default: false,
},
]);
if (answer.confirm) {
});
if (confirmed) {
fs.rmSync(_path, { recursive: true });
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 fs from 'fs';
import { chalk } from '@/module/chalk.ts';
import inquirer from 'inquirer';
import { confirm, input } from '@inquirer/prompts';
// 设置工作目录
const setWorkdir = async (options: { workdir: string }) => {
@@ -24,15 +24,11 @@ const setWorkdir = async (options: { workdir: string }) => {
console.log('路径不存在');
fs.mkdirSync(finalPath, { recursive: true });
}
const answers = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
const confirmed = await confirm({
message: `Are you sure you want to set the workdir to: ${finalPath}?`,
default: false,
},
]);
if (answers.confirm) {
});
if (confirmed) {
flag = true;
config.workdir = finalPath;
console.log(chalk.green(`set workdir success:`, finalPath));
@@ -81,15 +77,11 @@ const command = new Command('config')
console.log('路径不存在');
fs.mkdirSync(finalPath, { recursive: true });
}
const answers = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
const confirmed = await confirm({
message: `Are you sure you want to set the workdir to: ${finalPath}?`,
default: false,
},
]);
if (answers.confirm) {
});
if (confirmed) {
flag = true;
config.workdir = finalPath;
console.log(chalk.green(`set workdir success:`, finalPath));
@@ -100,15 +92,11 @@ const command = new Command('config')
if (options.set) {
const key = options.set;
let value = options.value;
const answer = await inquirer.prompt([
{
type: 'input',
name: 'value',
if (!value) {
value = await input({
message: `Enter your ${key}:(current: ${config[key]})`,
when: () => !value,
},
]);
value = answer.value || value;
});
}
if (key && value) {
flag = true;
config[key] = value;
@@ -138,16 +126,10 @@ const setCommand = new Command('set')
return;
}
let flag = false;
const answer = await inquirer.prompt([
{
type: 'input',
name: 'value',
if (value === 'not_input') {
value = await input({
message: `Enter your ${key}:(current: ${config[key]})`,
when: () => value === 'not_input',
},
]);
if (value === 'not_input' && !answer.value) {
value = '';
});
}
if (key === 'workdir') {
await setWorkdir({ workdir: value });
@@ -192,15 +174,11 @@ const getCommand = new Command('get')
.action(async (key) => {
const config = getConfig();
const keys = Object.keys(config);
const answer = await inquirer.prompt([
{
type: 'input',
name: 'key',
if (!key) {
key = await input({
message: `Enter your key:(keys: ${JSON.stringify(keys)})`,
when: () => !key,
},
]);
key = answer.key || key;
});
}
if (config[key]) {
console.log(chalk.green(`get ${key}:`));

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ import { getConfig, query } from '@/module/index.ts';
import { fileIsExist } from '@/uitls/file.ts';
import { chalk } from '@/module/chalk.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';
// 查找文件(忽略大小写)
async function findFileInsensitive(targetFile: string): Promise<string | null> {

View File

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

View File

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

View File

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

View File

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