Auto commit: 2026-03-24 13:04

This commit is contained in:
xiongxiao
2026-03-24 13:04:47 +08:00
committed by cnb
parent 8c8cf1aadf
commit b05f059aea
19 changed files with 2785 additions and 0 deletions

45
src/routes/ai.ts Normal file
View File

@@ -0,0 +1,45 @@
import { app } from '../app.ts';
import { app as aiApp } from '../ai/index.ts';
import { z } from 'zod';
import { chalk } from '@/module/chalk.ts';
import { logger } from '@/module/logger.ts';
const runCmd = async (cmd: string) => {
const res = await aiApp.router.call({ path: 'cmd-run', payload: { cmd } });
const { body } = res;
const steps = body?.steps || [];
for (const step of steps) {
logger.debug(chalk.blue(`\n==== 步骤: ${step.cmd || '结束'} ====`));
logger.debug(step.result || 'No result');
}
}
app.route({
path: 'ai',
key: 'run',
description: '执行 AI 命令',
metadata: {
args: {
cmd: z.string().optional().describe('要执行的 CMD 命令'),
}
}
}).define(async (ctx) => {
const { cmd } = ctx.args;
if (cmd) {
await runCmd(cmd);
} else {
console.log('请提供要执行的 CMD 命令');
}
}).addTo(app)
app.route({
path: 'ai',
key: 'deploy',
description: '部署 AI 后端应用',
metadata: {
args: {}
}
}).define(async () => {
const cmd = 'ev pack -p -u';
await runCmd(cmd);
}).addTo(app)

153
src/routes/app.ts Normal file
View File

@@ -0,0 +1,153 @@
import { app } from '../app.ts';
import { z } from 'zod';
import { chalk } from '@/module/chalk.ts';
import { queryApp } from '@/query/app-manager/query-app.ts';
import { installApp, uninstallApp, fetchLink } from '@/module/download/install.ts';
import { fileIsExist } from '@/uitls/file.ts';
import fs from 'fs';
import { getConfig } from '@/module/get-config.ts';
import path from 'path';
import { confirm } from '@inquirer/prompts';
import { getUrl } from '@/module/query.ts';
app.route({
path: 'app',
key: 'download',
description: '下载 app serve client的包',
metadata: {
args: {
id: z.string().optional().describe('下载 app serve client的包, id 或者user/key'),
output: z.string().optional().describe('输出路径'),
registry: z.string().optional().describe('使用私有源'),
force: z.boolean().optional().describe('强制覆盖'),
yes: z.boolean().optional().describe('覆盖的时候不提示'),
}
}
}).define(async (ctx) => {
const { id, output, registry, force, yes } = ctx.args;
if (!id) {
console.error(chalk.red('id is required'));
return;
}
if (output) {
const checkOutput = fileIsExist(output);
if (!checkOutput) {
fs.mkdirSync(output, { recursive: true });
}
}
const [user, key] = id.split('/');
const data: any = {};
if (user && key) {
data.user = user;
data.key = key;
} else {
data.id = id;
}
let regUrl = 'https://kevisual.cn';
if (registry) {
regUrl = new URL(registry).origin;
} else {
const config = getConfig();
regUrl = new URL(config.baseURL).origin;
}
const res = await queryApp(data, { url: getUrl(regUrl) });
console.log('registry', regUrl, data);
if (res.code === 200) {
const appData = res.data;
const result = await installApp(appData, {
appDir: output,
kevisualUrl: regUrl,
force,
yes,
});
if (result.code === 200) {
console.log(chalk.green('下载成功', res.data?.user, res.data?.key));
} else {
console.error(chalk.red(result.message || '下载失败'));
}
} else {
console.error(chalk.red(res.message || '下载失败'));
}
}).addTo(app)
app.route({
path: 'app',
key: 'uninstall',
alias: 'remove',
description: '卸载 app serve client的包',
metadata: {
args: {
id: z.string().optional().describe('user/key'),
path: z.string().optional().describe('删除的路径'),
}
}
}).define(async (ctx) => {
const { id, path: deletePath } = ctx.args;
if (deletePath) {
const resolvedPath = path.resolve(deletePath);
try {
const checkPath = fileIsExist(resolvedPath);
if (!checkPath) {
console.error(chalk.red('path is error, 请输入正确的路径'));
} else {
const confirmed = await confirm({
message: `确定要删除 ${resolvedPath} 吗?`,
default: false,
});
if (confirmed) {
fs.rmSync(resolvedPath, { recursive: true });
console.log(chalk.green('删除成功', resolvedPath));
}
}
} catch (e) {
console.error(chalk.red('删除失败', e));
}
return;
}
if (!id) {
console.error(chalk.red('id is required'));
return;
}
const [user, key] = id.split('/');
if (!user || !key) {
console.error(chalk.red('id is required user/key'));
return;
}
const result = await uninstallApp(
{
user,
key,
},
{
appDir: '',
},
);
if (result.code === 200) {
console.log(chalk.green('卸载成功', user, key));
} else {
console.error(chalk.red(result.message || '卸载失败'));
}
}).addTo(app)
app.route({
path: 'app',
key: 'link',
description: '从 URL 链接应用',
metadata: {
args: {
url: z.string().describe('URL'),
output: z.string().optional().describe('输出目录'),
}
}
}).define(async (ctx) => {
const { url, output } = ctx.args;
const { content, filename } = await fetchLink(url, { returnContent: true });
if (output) {
const checkOutput = fileIsExist(output);
if (!checkOutput) {
fs.mkdirSync(output, { recursive: true });
}
}
fs.writeFileSync(path.join(output || '.', filename), content);
}).addTo(app)

148
src/routes/cc.ts Normal file
View File

@@ -0,0 +1,148 @@
import { app } from '../app.ts';
import { z } from 'zod';
import { chalk } from '@/module/chalk.ts';
import path from 'node:path';
import os from 'node:os';
import fs from 'node:fs';
import { select } from '@inquirer/prompts';
const MODELS = ['minimax', 'glm', 'volcengine', 'bailian'] as const;
const changeMinimax = (token?: string) => {
const auth_token = token || useKey("MINIMAX_API_KEY")
const MINIMAX_MODEL = useKey("MINIMAX_MODEL") || "MiniMax-M2.5"
return {
"env": {
"ANTHROPIC_AUTH_TOKEN": auth_token,
"ANTHROPIC_BASE_URL": "https://api.minimaxi.com/anthropic",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": MINIMAX_MODEL,
"ANTHROPIC_DEFAULT_OPUS_MODEL": MINIMAX_MODEL,
"ANTHROPIC_DEFAULT_SONNET_MODEL": MINIMAX_MODEL,
"ANTHROPIC_MODEL": MINIMAX_MODEL,
"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"
}
}
}
const changeVolcengine = (token?: string) => {
const auth_token = token || useKey('VOLCENGINE_API_KEY')
return {
"env": {
"ANTHROPIC_AUTH_TOKEN": auth_token,
"ANTHROPIC_BASE_URL": "https://ark.cn-beijing.volces.com/api/coding",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "ark-code-latest",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "ark-code-latest",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "ark-code-latest",
"ANTHROPIC_MODEL": "ark-code-latest",
"API_TIMEOUT_MS": "3000000"
}
}
}
const changeBailian = (token?: string) => {
const auth_token = token || useKey('BAILIAN_API_KEY')
return {
"env": {
"ANTHROPIC_AUTH_TOKEN": auth_token,
"ANTHROPIC_BASE_URL": "https://coding.dashscope.aliyuncs.com/apps/anthropic",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "qwen3-coder-plus",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "qwen3-coder-plus",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "qwen3-coder-plus",
"ANTHROPIC_MODEL": "qwen3-coder-plus"
},
"includeCoAuthoredBy": false
}
}
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<string, (token?: string) => object> = {
minimax: changeMinimax,
glm: changeGLM,
volcengine: changeVolcengine,
bailian: changeBailian,
};
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));
};
app.route({
path: 'cc',
key: 'main',
description: '切换claude code模型',
metadata: {
args: {
models: z.string().optional().describe(`选择模型: ${MODELS.join(' | ')}`),
}
}
}).define(async (ctx) => {
const configPath = path.join(os.homedir(), '.claude/settings.json');
const config = readOrCreateConfig(configPath);
let selectedModel: string;
if (ctx.args.models && MODELS.includes(ctx.args.models as any)) {
selectedModel = ctx.args.models;
} 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}`);
}).addTo(app)

140
src/routes/ccc.ts Normal file
View File

@@ -0,0 +1,140 @@
import { app } from '../app.ts';
import { z } from 'zod';
import { chalk } from '@/module/chalk.ts';
import path from 'node:path';
import os from 'node:os';
import fs from 'node:fs';
import { select } from '@inquirer/prompts';
type ProviderConfig = {
npm: string;
name: string;
models: Record<string, { name: string }>;
options?: {
baseURL?: string;
apiKey?: string;
[key: string]: unknown;
};
};
type OpencodeConfig = {
$schema?: string;
autoshare?: boolean;
share?: string;
autoupdate?: boolean;
permission?: string;
model?: string;
watcher?: {
ignore?: string[];
};
plugin?: string[];
provider?: Record<string, ProviderConfig>;
};
const readOpencodeConfig = (configPath: string): OpencodeConfig => {
if (fs.existsSync(configPath)) {
const content = fs.readFileSync(configPath, 'utf-8');
try {
return JSON.parse(content);
} catch {
return { provider: {} };
}
}
return { provider: {} };
};
const saveOpencodeConfig = (configPath: string, config: OpencodeConfig) => {
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
};
const extractAvailableModels = (config: OpencodeConfig): Array<{ name: string; provider: string; model: string; label: string }> => {
const models: Array<{ name: string; provider: string; model: string; label: string }> = [];
const providers = config.provider || {};
for (const [providerKey, providerConfig] of Object.entries(providers)) {
const providerModels = providerConfig.models || {};
for (const [modelKey] of Object.entries(providerModels)) {
models.push({
name: providerConfig.name,
provider: providerKey,
model: modelKey,
label: `${providerKey}/${modelKey}`,
});
}
}
return models;
};
app.route({
path: 'ccc',
key: 'main',
description: '切换 opencode 模型',
metadata: {
args: {
model: z.string().optional().describe('选择模型 (格式: provider/model)'),
}
}
}).define(async (ctx) => {
const configPath = path.join(os.homedir(), '.config', 'opencode', 'opencode.json');
const config = readOpencodeConfig(configPath);
const availableModels = extractAvailableModels(config);
if (availableModels.length === 0) {
console.log(chalk.red('没有找到可用的模型配置,请检查 opencode.json 中的 provider 配置'));
return;
}
let selectedModel: string;
if (ctx.args.model) {
selectedModel = ctx.args.model;
} else {
selectedModel = await select({
message: '请选择模型:',
choices: availableModels.map((m) => ({
name: `${m.name} - ${m.model}`,
value: m.label,
})),
});
}
const validModel = availableModels.find((m) => m.label === selectedModel);
if (!validModel) {
console.log(chalk.red(`无效的模型选择: ${selectedModel}`));
return;
}
config.model = selectedModel;
saveOpencodeConfig(configPath, config);
console.log(`已切换到模型: ${chalk.green(selectedModel)}`);
console.log(`提供商: ${chalk.cyan(validModel.name)}`);
console.log(`配置已保存到: ${configPath}`);
}).addTo(app)
app.route({
path: 'ccc',
key: 'show',
description: '显示当前 opencode 配置的 model',
metadata: {
args: {}
}
}).define(async () => {
const configPath = path.join(os.homedir(), '.config', 'opencode', 'opencode.json');
const config = readOpencodeConfig(configPath);
if (!config.model) {
console.log(chalk.yellow('当前没有配置 model'));
return;
}
const availableModels = extractAvailableModels(config);
const currentModel = availableModels.find((m) => m.label === config.model);
console.log(chalk.bold('当前 opencode 配置:'));
console.log(`模型: ${chalk.green(config.model)}`);
if (currentModel) {
console.log(`提供商: ${chalk.cyan(currentModel.name)}`);
}
console.log(`配置文件: ${configPath}`);
}).addTo(app)

82
src/routes/cnb.ts Normal file
View File

@@ -0,0 +1,82 @@
import { app } from '../app.ts';
import { z } from 'zod';
import { createKeepAlive } from '@kevisual/cnb/keep'
import { readFileSync } from 'node:fs'
import path from 'node:path'
app.route({
path: 'cnb',
key: 'live',
description: '启动 CNB Keep Alive 服务',
metadata: {
args: {
json: z.string().optional().describe('JSON数据'),
config: z.string().optional().describe('配置文件路径'),
}
}
}).define(async (ctx) => {
let { json, config: configPath } = ctx.args;
configPath = configPath || 'keep.json';
if (configPath!.startsWith('/')) {
configPath = path.resolve(configPath!)
} else {
configPath = path.join(process.cwd(), configPath!)
}
try {
let jsonString = json;
if (!jsonString) {
jsonString = readFileSync(configPath!, 'utf-8').trim();
}
const config = JSON.parse(jsonString!);
if (!config.wss || !config.cookie) {
console.error('配置错误: 必须包含 wss 和 cookie 字段');
process.exit(1);
}
createKeepAlive({
wsUrl: config.wss,
cookie: config.cookie,
onDisconnect: (code) => {
console.log(`与 CNB 服务器断开连接,代码: ${code}`);
},
debug: true
});
} catch (error) {
console.error('JSON 解析错误: 请检查输入的 JSON 格式是否正确');
process.exit(1);
}
}).addTo(app)
app.route({
path: 'cnb',
key: 'workspace',
alias: 'w',
description: '工作区live保活',
metadata: {
args: {}
}
}).define(async () => {
try {
const configPath = path.join('/workspace/live/keep.json')
const json = readFileSync(configPath, 'utf-8').trim();
const config = JSON.parse(json);
if (!config.wss || !config.cookie) {
console.error('配置错误: 必须包含 wss 和 cookie 字段');
process.exit(1);
}
createKeepAlive({
wsUrl: config.wss,
cookie: config.cookie,
onDisconnect: (code) => {
console.log(`与 CNB 服务器断开连接,代码: ${code}`);
},
debug: true
});
} catch (e) {
console.error('error', e)
}
}).addTo(app)

228
src/routes/config.ts Normal file
View File

@@ -0,0 +1,228 @@
import { app } from '../app.ts';
import { z } from 'zod';
import { checkFileExists, getConfig, writeConfig } from '@/module/index.ts';
import path from 'path';
import fs from 'fs';
import { chalk } from '@/module/chalk.ts';
import { confirm, input } from '@inquirer/prompts';
const setWorkdir = async (workdir: string) => {
const execPath = process.cwd();
let flag = false;
let config = getConfig();
let finalPath: string;
if (workdir.startsWith('/')) {
finalPath = workdir;
} else {
finalPath = path.join(execPath, workdir);
}
if (!checkFileExists(finalPath)) {
console.log('路径不存在');
fs.mkdirSync(finalPath, { recursive: true });
}
const confirmed = await confirm({
message: `Are you sure you want to set the workdir to: ${finalPath}?`,
default: false,
});
if (confirmed) {
flag = true;
config.workdir = finalPath;
console.log(chalk.green(`set workdir success:`, finalPath));
} else {
console.log('Cancel set workdir');
}
if (flag) {
writeConfig(config);
}
};
app.route({
path: 'config',
key: 'main',
description: 'config 命令',
metadata: {
args: {
dev: z.string().optional().describe('Specify dev'),
set: z.string().optional().describe('set config'),
get: z.string().optional().describe('get config'),
remove: z.string().optional().describe('remove config'),
value: z.string().optional().describe('value'),
workdir: z.string().optional().describe('web config'),
}
}
}).define(async (ctx) => {
const { dev, workdir, set, get: getKey, remove, value } = ctx.args;
let config = getConfig();
let flag = false;
const execPath = process.cwd();
if (dev === 'true' || dev === 'false') {
flag = true;
if (dev === 'true') {
config.dev = true;
} else {
config.dev = false;
}
}
if (workdir) {
let finalPath: string;
if (workdir.startsWith('/')) {
finalPath = workdir;
} else {
finalPath = path.join(execPath, workdir);
}
if (!checkFileExists(finalPath)) {
console.log('路径不存在');
fs.mkdirSync(finalPath, { recursive: true });
}
const confirmed = await confirm({
message: `Are you sure you want to set the workdir to: ${finalPath}?`,
default: false,
});
if (confirmed) {
flag = true;
config.workdir = finalPath;
console.log(chalk.green(`set workdir success:`, finalPath));
} else {
console.log('Cancel set workdir');
}
}
if (set) {
let val = value;
if (!val) {
val = await input({
message: `Enter your ${set}:(current: ${config[set as keyof typeof config]})`,
});
}
if (set && val) {
flag = true;
config[set] = val;
}
}
if (remove) {
delete config[remove];
flag = true;
}
if (flag) {
writeConfig(config);
}
}).addTo(app)
app.route({
path: 'config',
key: 'set',
description: 'set config',
metadata: {
args: {
key: z.string().describe('配置键名'),
value: z.string().optional().describe('配置值'),
}
}
}).define(async (ctx) => {
const { key, value } = ctx.args;
const config = getConfig();
if (!key) {
console.log('key is empty');
return;
}
let flag = false;
let val = value || 'not_input';
if (val === 'not_input') {
val = await input({
message: `Enter your ${key}:(current: ${config[key as keyof typeof config]})`,
});
}
if (key === 'workdir') {
await setWorkdir(val);
return;
}
const transformValue = (val: string) => {
if (val === 'true') {
return true;
}
if (val === 'false') {
return false;
}
if (!isNaN(Number(val))) {
return Number(val);
}
return val;
};
const newValue = transformValue(val);
if (key && val) {
flag = true;
if (key === 'dev') {
if (val === 'true') {
config.dev = true;
} else {
config.dev = false;
}
} else {
config[key] = val;
}
console.log(chalk.green(`set ${key} success:`, config.key));
}
if (flag) {
writeConfig(config);
}
}).addTo(app)
app.route({
path: 'config',
key: 'get',
description: 'get config',
metadata: {
args: {
key: z.string().optional().describe('配置键名'),
}
}
}).define(async (ctx) => {
const { key } = ctx.args;
const config = getConfig();
const keys = Object.keys(config);
let selectedKey = key;
if (!selectedKey) {
selectedKey = await input({
message: `Enter your key:(keys: ${JSON.stringify(keys)})`,
});
}
if (config[selectedKey as keyof typeof config]) {
console.log(chalk.green(`get ${selectedKey}:`));
console.log(config[selectedKey as keyof typeof config]);
} else {
console.log(chalk.red(`not found ${selectedKey}`));
}
}).addTo(app)
app.route({
path: 'config',
key: 'remove',
description: 'remove config',
metadata: {
args: {
key: z.string().describe('配置键名'),
}
}
}).define(async (ctx) => {
const { key } = ctx.args;
const config = getConfig();
if (key) {
delete config[key];
writeConfig(config);
console.log(chalk.green(`remove ${key} success`));
}
}).addTo(app)
app.route({
path: 'config',
key: 'list',
description: 'list config',
metadata: {
args: {}
}
}).define(async () => {
const config = getConfig();
console.log(chalk.green('config', JSON.stringify(config, null, 2)));
}).addTo(app)

250
src/routes/deploy.ts Normal file
View File

@@ -0,0 +1,250 @@
import { app } from '../app.ts';
import { z } from 'zod';
import glob from 'fast-glob';
import path from 'path';
import fs from 'fs';
import FormData from 'form-data';
import { getBaseURL, query, storage } from '@/module/query.ts';
import chalk from 'chalk';
import { upload } from '@/module/download/upload.ts';
import { getHash } from '@/uitls/hash.ts';
import { queryAppVersion } from '@/query/app-manager/query-app.ts';
import { logger } from '@/module/logger.ts';
import { getUsername } from './login.ts';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 8);
export const getPackageJson = (opts?: { version?: string; appKey?: string }) => {
const filePath = path.join(process.cwd(), 'package.json');
if (!fs.existsSync(filePath)) {
return null;
}
try {
const packageJson = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
const basename = packageJson.basename || '';
const version = packageJson.version || '1.0.0';
const pkgApp = packageJson.app;
const userAppArry = basename.split('/');
if (userAppArry.length <= 2 && !opts?.appKey) {
console.error(chalk.red('basename is error, 请输入正确的路径, packages.json中basename例如 /root/appKey'));
return null;
}
const [user, appKey] = userAppArry;
return { basename, version, pkg: packageJson, user, appKey: opts?.appKey || appKey, app: pkgApp };
} catch (error) {
return null;
}
};
type UploadFileOptions = {
key: string;
version: string;
username: string;
directory?: string;
};
export const uploadFilesV2 = async (files: string[], uploadDir: string, opts: UploadFileOptions): Promise<any> => {
const { key, version, username, directory: prefix } = opts || {};
for (let i = 0; i < files.length; i++) {
const file = files[i];
const filePath = path.join(uploadDir, file);
logger.info('[上传进度]', `${i + 1}/${files.length}`, file);
const form = new FormData();
const filename = path.basename(filePath);
const encodedFilename = Buffer.from(filename, 'utf-8').toString('binary');
form.append('file', fs.createReadStream(filePath), {
filename: encodedFilename,
filepath: file,
});
const _baseURL = getBaseURL();
const url = new URL(`/${username}/resources/${key}/${version}/${prefix ? prefix + '/' : ''}${file}`, _baseURL);
const token = await storage.getItem('token');
const check = () => {
const checkUrl = new URL(url.toString());
checkUrl.searchParams.set('stat', '1');
const res = query
.adapter({ url: checkUrl.toString(), method: 'GET', headers: { Authorization: 'Bearer ' + token } })
return res;
}
const checkRes = await check();
let needUpload = false;
let hash = '';
if (checkRes?.code === 404) {
needUpload = true;
hash = getHash(filePath);
} else if (checkRes?.code === 200) {
const etag = checkRes?.data?.etag;
hash = getHash(filePath);
if (etag !== hash) {
needUpload = true;
}
}
if (needUpload) {
url.searchParams.append('hash', hash);
const res = await upload({ url: url, form: form, token: token });
logger.debug('upload file', file, res);
if (res.code !== 200) {
logger.error('文件上传失败', file, res);
return { code: 500, message: '文件上传失败', file, fileRes: res };
}
} else {
console.log(chalk.green('\t 文件已经上传过了', url.toString()));
}
}
return { code: 200 }
}
export const deployLoadFn = async (id: string, org?: string) => {
if (!id) {
console.error(chalk.red('id is required'));
return;
}
const res = await query.post({
path: 'app',
key: 'publish',
data: {
id: id,
username: org,
detect: true,
},
});
if (res.code === 200) {
logger.info(chalk.green('deploy-load success. current version:', res.data?.version));
try {
const { user, key } = res.data;
const baseURL = getBaseURL();
const deployURL = new URL(`/${user}/${key}/`, baseURL);
logger.info(chalk.blue('deployURL', deployURL.href));
} catch (error) { }
} else {
logger.error('deploy-load failed', res.message);
}
};
app.route({
path: 'deploy',
key: 'main',
description: '把前端文件传到服务器',
metadata: {
args: {
filePath: z.string().describe('Path to the file to be uploaded'),
version: z.string().optional().describe('verbose'),
key: z.string().optional().describe('key'),
yes: z.boolean().optional().describe('yes'),
org: z.string().optional().describe('org'),
update: z.boolean().optional().describe('load current app'),
showBackend: z.boolean().optional().describe('show backend url'),
dot: z.boolean().optional().describe('是否上传隐藏文件'),
directory: z.string().optional().describe('上传的prefix路径'),
}
}
}).define(async (ctx) => {
const { filePath, version: optVersion, key: optKey, update, org, showBackend, dot, directory } = ctx.args;
try {
let version = optVersion;
let key = optKey;
const pkgInfo = getPackageJson({ version, appKey: key });
if (!version && pkgInfo?.version) {
version = pkgInfo?.version || '1.0.0';
}
if (!key && pkgInfo?.appKey) {
key = pkgInfo?.appKey || '';
}
logger.debug('start deploy');
if (!version) {
version = '1.0.0';
}
if (!key) {
key = nanoid(8);
}
const pwd = process.cwd();
const deployDir = path.join(pwd, filePath);
const stat = fs.statSync(deployDir);
let _relativeFiles: string[] = [];
let isDirectory = false;
if (stat.isDirectory()) {
isDirectory = true;
const files = await glob('**/*', {
cwd: deployDir,
ignore: ['node_modules/**/*', '.git/**/*', '.DS_Store'],
onlyFiles: true,
dot: dot || false,
absolute: true,
});
const normalizeFilePath = (f: string) => f.split(path.sep).join('/');
_relativeFiles = files.map((file) => {
const relativePath = path.relative(deployDir, file);
return normalizeFilePath(relativePath);
});
} else if (stat.isFile()) {
const filename = path.basename(deployDir);
_relativeFiles = [filename];
}
logger.debug('upload Files', _relativeFiles);
logger.debug('upload Files Key', key, version);
let username = '';
if (pkgInfo?.user) {
username = pkgInfo.user;
} else if (org) {
username = org;
} else {
const me = await getUsername();
if (me) {
username = me;
} else {
logger.error('无法获取用户名,请使用先登录');
return;
}
}
const uploadDirectory = isDirectory ? deployDir : path.dirname(deployDir);
const res = await uploadFilesV2(_relativeFiles, uploadDirectory, { key, version, username: username, directory });
logger.debug('upload res', res);
if (res?.code === 200) {
const res2 = await queryAppVersion({
key: key,
version: version,
create: true
});
logger.debug('queryAppVersion res', res2, key, version);
if (res2.code !== 200) {
console.error(chalk.red('查询应用版本失败'), res2.message, key);
return;
}
const { id, ...rest } = res2.data || {};
if (id && !update) {
if (!org) {
console.log(chalk.green(`更新为最新版本: envision deploy-load ${id}`));
} else {
console.log(chalk.green(`更新为最新版本: envision deploy-load ${id} -o ${org}`));
}
} else if (id && update) {
deployLoadFn(id);
}
logger.debug('deploy success', res2.data);
if (id && showBackend) {
console.log(chalk.blue('下一个步骤服务端应用部署:\n'), 'envision pack-deploy', id);
}
} else {
console.error('File upload failed', res?.message);
}
} catch (error) {
console.error('error', error);
}
}).addTo(app)
app.route({
path: 'deploy',
key: 'load',
description: '部署加载',
metadata: {
args: {
id: z.string().describe('id'),
org: z.string().optional().describe('org'),
}
}
}).define(async (ctx) => {
const { id, org } = ctx.args;
deployLoadFn(id, org);
}).addTo(app)

90
src/routes/docker.ts Normal file
View File

@@ -0,0 +1,90 @@
import { app } from '../app.ts';
import { z } from 'zod';
import { chalk } from '@/module/chalk.ts';
import { spawn } from 'node:child_process';
import { useKey } from '@kevisual/use-config';
app.route({
path: 'docker',
key: 'login',
description: '登录 Docker 镜像仓库',
metadata: {
args: {
registry: z.string().optional().describe('Docker 镜像仓库地址'),
}
}
}).define(async (ctx) => {
const registry = ctx.args.registry || 'default';
let DOCKER_USERNAME = useKey('DOCKER_USERNAME') as string;
let DOCKER_PASSWORD = useKey('DOCKER_PASSWORD') as string;
let DOCKER_REGISTRY = useKey('DOCKER_REGISTRY') as string;
if (registry !== 'default') {
DOCKER_USERNAME = 'cnb';
DOCKER_PASSWORD = useKey('CNB_TOKEN') as string;
DOCKER_REGISTRY = 'docker.cnb.cool';
}
if (!DOCKER_USERNAME || !DOCKER_PASSWORD) {
console.log(chalk.red('请先配置 DOCKER_USERNAME 和 DOCKER_PASSWORD'));
return;
}
const loginProcess = spawn('docker', [
'login',
'--username',
DOCKER_USERNAME,
DOCKER_REGISTRY,
'--password-stdin'
], {
stdio: ['pipe', 'inherit', 'inherit']
});
loginProcess.stdin.write(DOCKER_PASSWORD + '\n');
loginProcess.stdin.end();
loginProcess.on('close', (code) => {
if (code === 0) {
console.log(chalk.green('登录成功'));
} else {
console.log(chalk.red(`登录失败,退出码:${code}`));
}
});
}).addTo(app)
app.route({
path: 'helm',
key: 'login',
description: '登录 Helm 镜像仓库',
metadata: {
args: {}
}
}).define(async () => {
let DOCKER_USERNAME = 'cnb';
let DOCKER_PASSWORD = useKey('CNB_TOKEN') as string;
if (!DOCKER_PASSWORD) {
console.log(chalk.red('请先配置 CNB_TOKEN'));
return;
}
const helmLoginProcess = spawn('helm', [
'registry',
'login',
'--username',
DOCKER_USERNAME,
'--password-stdin',
'helm.cnb.cool'
], {
stdio: ['pipe', 'inherit', 'inherit']
});
helmLoginProcess.stdin.write(DOCKER_PASSWORD + '\n');
helmLoginProcess.stdin.end();
helmLoginProcess.on('close', (code) => {
if (code === 0) {
console.log(chalk.green('Helm 登录成功'));
} else {
console.log(chalk.red(`Helm 登录失败,退出码:${code}`));
}
});
}).addTo(app)

103
src/routes/download.ts Normal file
View File

@@ -0,0 +1,103 @@
import { app } from '../app.ts';
import { z } from 'zod';
import path from 'path';
import fs from 'fs';
import { queryLogin } from '@/module/query.ts';
import { fetchLink } from '@/module/download/install.ts';
import { chalk } from '@/module/chalk.ts';
export type FileItem = {
name: string;
size: number;
lastModified: string;
etag: string;
path: string;
pathname: string;
url: string;
};
app.route({
path: 'download',
key: 'main',
description: '下载项目',
metadata: {
args: {
link: z.string().optional().describe('下载链接'),
directory: z.string().optional().describe('下载目录'),
}
}
}).define(async (ctx) => {
let { link, directory } = ctx.args;
if (!link) {
console.log('请提供下载链接');
return;
}
let url = new URL(link);
if (!url.pathname.endsWith('/')) {
url.pathname += '/';
}
url.searchParams.set('recursive', 'true');
const downloadDir = directory || process.cwd();
const token = await queryLogin.getToken();
const res = await queryLogin.query.fetchText({
url: url.toString(),
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
});
if (res.code === 200 && res.data) {
const files = res.data as FileItem[];
console.log(`获取到 ${files.length} 个文件`);
await downloadFiles(files, { directory: downloadDir });
} else {
console.log(chalk.red('获取文件列表失败:'), res.message || '未知错误');
}
}).addTo(app)
export const downloadFiles = async (files: FileItem[], opts?: { directory?: string }) => {
const directory = opts?.directory || process.cwd();
let successCount = 0;
let failCount = 0;
for (const file of files) {
try {
const downloadPath = path.join(directory, file.path);
const dir = path.dirname(downloadPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
console.log(`下载中: ${file.name}`);
const { blob, type } = await fetchLink(file.url);
if (type.includes('text/html')) {
const text = await blob.text();
if (text === 'fetchRes is error') {
console.log(chalk.red('下载失败:'), file.name);
failCount++;
continue;
}
}
fs.writeFileSync(downloadPath, Buffer.from(await blob.arrayBuffer()));
successCount++;
console.log(chalk.green('下载成功:'), file.name);
} catch (error) {
failCount++;
console.log(chalk.red('下载失败:'), file.name, error);
}
}
console.log(chalk.blue('下载完成'));
console.log(chalk.green(`成功: ${successCount}`));
console.log(chalk.red(`失败: ${failCount}`));
return {
successCount,
failCount,
};
};

82
src/routes/gist.ts Normal file
View File

@@ -0,0 +1,82 @@
import { app } from '../app.ts';
import { z } from 'zod';
import path from 'node:path';
import fs from 'node:fs';
import { spawn } from 'child_process';
import { chalk } from '@/module/chalk.ts';
import { getHeader } from '@/module/query.ts';
app.route({
path: 'gist',
key: 'main',
description: '同步片段代码',
metadata: {
args: {
dir: z.string().optional().describe('配置目录'),
link: z.string().describe('链接'),
}
}
}).define(async (ctx) => {
const { dir, link } = ctx.args;
if (!link) {
console.log(chalk.red('请提供链接'));
return;
}
const resolvedDir = path.resolve(dir || process.cwd());
if (!fs.existsSync(resolvedDir)) {
fs.mkdirSync(resolvedDir, { recursive: true });
}
const cmd = `ev gist download -l ${link} -s `
console.log(chalk.green('开始执行'), cmd);
spawn(cmd, {
shell: true, stdio: 'inherit',
cwd: resolvedDir
});
}).addTo(app)
app.route({
path: 'gist',
key: 'download',
description: '克隆代码片段',
metadata: {
args: {
dir: z.string().optional().describe('配置目录'),
config: z.string().optional().describe('配置文件的名字'),
sync: z.boolean().optional().describe('下载配置成功后,是否需要同步文件'),
link: z.string().optional().describe('下载配置链接'),
}
}
}).define(async (ctx) => {
const { dir, config: configFilename, sync, link } = ctx.args;
console.log('克隆代码片段');
const resolvedDir = path.resolve(dir || process.cwd());
const configPath = path.join(resolvedDir, configFilename || 'kevisual.json');
if (!link) {
console.log(chalk.red('请提供链接'));
return;
}
const res = await fetch(link, {
headers: await getHeader(),
}).then(res => {
return res.json();
}).catch((err) => {
console.log(chalk.red('配置文件下载失败'));
throw '配置文件下载失败';
});
fs.mkdirSync(resolvedDir, { recursive: true });
fs.writeFileSync(configPath, JSON.stringify(res, null, 2));
console.log(chalk.green('配置文件下载成功: ' + configPath));
if (sync) {
const cmd = `ev sync download --config "${configFilename || 'kevisual.json'}"`;
console.log(chalk.green('开始同步文件'), cmd);
spawn(cmd, {
cwd: resolvedDir,
shell: true,
stdio: 'inherit',
});
}
}).addTo(app)

79
src/routes/jwks.ts Normal file
View File

@@ -0,0 +1,79 @@
import { app } from '../app.ts';
import { z } from 'zod';
import { generate } from '@kevisual/auth'
import fs from 'node:fs';
import path from 'node:path';
export const getPath = async (dir: string) => {
const JWKS_PATH = path.join(dir, 'jwks.json');
const PRIVATE_JWK_PATH = path.join(dir, 'privateKey.json');
const PRIVATE_KEY_PATH = path.join(dir, 'privateKey.txt');
const PUBLIC_KEY_PATH = path.join(dir, 'publicKey.txt');
return {
JWKS_PATH,
PRIVATE_JWK_PATH,
PRIVATE_KEY_PATH,
PUBLIC_KEY_PATH,
}
}
app.route({
path: 'jwks',
key: 'generate',
alias: 'gen',
description: '生成 JWKS 密钥对',
metadata: {
args: {
dir: z.string().optional().describe('指定保存目录'),
}
}
}).define(async (ctx) => {
const dir = ctx.args.dir || 'jwt';
const absDir = path.isAbsolute(dir) ? dir : path.join(process.cwd(), dir);
if (!fs.existsSync(absDir)) {
fs.mkdirSync(absDir, { recursive: true });
}
const { JWKS_PATH, PRIVATE_JWK_PATH, PRIVATE_KEY_PATH, PUBLIC_KEY_PATH } = await getPath(absDir);
const { jwks, privateJWK, privatePEM, publicPEM } = await generate();
fs.writeFileSync(PUBLIC_KEY_PATH, publicPEM);
fs.writeFileSync(PRIVATE_KEY_PATH, privatePEM);
fs.writeFileSync(PRIVATE_JWK_PATH, JSON.stringify(privateJWK, null, 2));
fs.writeFileSync(JWKS_PATH, JSON.stringify(jwks, null, 2));
console.log(`Keys have been saved to directory: ${absDir}`);
}).addTo(app)
app.route({
path: 'jwks',
key: 'get',
description: '获取 JWKS 内容',
metadata: {
args: {
dir: z.string().optional().describe('指定 JWKS 所在目录'),
type: z.string().optional().describe('指定获取类型jwks 或 privateJWK'),
}
}
}).define(async (ctx) => {
const dir = ctx.args.dir || 'jwt';
const absDir = path.isAbsolute(dir) ? dir : path.join(process.cwd(), dir);
const { JWKS_PATH, PRIVATE_JWK_PATH } = await getPath(absDir);
const type = ctx.args.type || 'jwks';
if (type !== 'jwks') {
if (!fs.existsSync(PRIVATE_JWK_PATH)) {
console.error(`Private JWK file not found in directory: ${absDir}`);
return;
}
const privateJWKContent = fs.readFileSync(PRIVATE_JWK_PATH, 'utf-8');
console.log('Private JWK:\n');
console.log(privateJWKContent);
return;
}
if (!fs.existsSync(JWKS_PATH)) {
console.error(`JWKS file not found in directory: ${absDir}`);
return;
}
const jwksContent = fs.readFileSync(JWKS_PATH, 'utf-8');
console.log('PublicJWKS:\n');
console.log(jwksContent);
}).addTo(app)

267
src/routes/npm.ts Normal file
View File

@@ -0,0 +1,267 @@
import { app } from '../app.ts';
import { z } from 'zod';
import { chalk } from '@/module/chalk.ts';
import path from 'path';
import { spawn } from 'child_process';
import { fileIsExist } from '@/uitls/file.ts';
import { getConfig } from '@/module/get-config.ts';
import fs from 'fs';
import { select, confirm } from '@inquirer/prompts';
import { checkPnpm } from '@/uitls/npm.ts';
const parseIfJson = (str: string) => {
try {
return JSON.parse(str);
} catch (e) {
return {};
}
};
const publishRegistry = (options: { execPath: string, registry: string, tag?: string, config: any, env: any }) => {
const packageJson = path.resolve(options.execPath, 'package.json');
let cmd = '';
const config = options.config || {};
const execPath = options.execPath;
const registry = options.registry;
const setEnv = options.env || {};
switch (registry) {
case 'npm':
cmd = 'npm publish -s --registry https://registry.npmjs.org';
break;
case 'cnb':
cmd = 'npm publish -s --registry https://npm.cnb.cool/kevisual/registry/packages/';
break;
default:
cmd = 'npm publish -s --registry https://registry.npmjs.org';
break;
}
if (fileIsExist(packageJson)) {
const keys = Object.keys(config).filter((key) => key.includes('NPM_TOKEN'));
const tokenEnv = keys.reduce((prev, key) => {
return {
...prev,
[key]: config[key],
};
}, {});
const pkg = fs.readFileSync(packageJson, 'utf-8');
const pkgJson = parseIfJson(pkg);
const version = pkgJson?.version as string;
if (version && options?.tag) {
let tag = String(version).split('-')[1] || '';
if (tag) {
if (tag.includes('.')) {
tag = tag.split('.')[0];
}
cmd = `${cmd} --tag ${tag}`;
}
}
console.log(chalk.green(cmd));
const child = spawn(cmd, {
shell: true,
cwd: execPath,
env: {
...process.env,
...tokenEnv,
...setEnv,
},
});
child.stdout.on('data', (data) => {
console.log(chalk.green(`${data}`));
});
child.stderr.on('data', (data) => {
if (data.toString().includes('npm notice')) {
console.log(chalk.yellow(`notice: ${data}`));
} else {
console.error(`stderr: ${data}`);
}
});
} else {
console.error(chalk.red('package.json not found'));
}
}
const patchFunc = (opts?: { directory?: string }) => {
const cwd = opts?.directory || process.cwd();
const packageJson = path.resolve(cwd, 'package.json');
if (fileIsExist(packageJson)) {
const pkg = fs.readFileSync(packageJson, 'utf-8');
const pkgJson = parseIfJson(pkg);
const version = pkgJson?.version as string;
if (version) {
const versionArr = String(version).split('.');
if (versionArr.length === 3) {
const patchVersion = Number(versionArr[2]) + 1;
const newVersion = `${versionArr[0]}.${versionArr[1]}.${patchVersion}`;
pkgJson.version = newVersion;
fs.writeFileSync(packageJson, JSON.stringify(pkgJson, null, 2));
console.log(chalk.green(`${pkgJson?.name} 更新到版本: ${newVersion}`));
}
}
}
}
app.route({
path: 'npm',
key: 'publish',
description: '发布 npm 包',
metadata: {
args: {
registry: z.string().optional().describe('发布源'),
proxy: z.boolean().optional().describe('使用代理'),
tag: z.boolean().optional().describe('使用 tag'),
update: z.boolean().optional().describe('更新新版本'),
}
}
}).define(async (ctx) => {
let { registry, proxy, tag, update } = ctx.args;
if (!registry) {
registry = await select({
message: 'Select the registry to publish',
choices: [
{ name: 'all', value: 'all' },
{ name: 'npm', value: 'npm' },
{ name: 'cnb', value: 'cnb' }
],
});
}
const config = getConfig();
const execPath = process.cwd();
let setEnv: any = {};
const proxyEnv = {
https_proxy: 'http://127.0.0.1:7890',
http_proxy: 'http://127.0.0.1:7890',
all_proxy: 'socks5://127.0.0.1:7890',
...config?.proxy,
};
if (proxy) {
setEnv = { ...proxyEnv };
}
if (update) {
patchFunc({ directory: execPath });
}
if (registry === 'all') {
publishRegistry({ execPath, registry: 'npm', config, env: setEnv });
publishRegistry({ execPath, registry: 'cnb', config, env: setEnv });
} else {
publishRegistry({ execPath, registry, tag, config, env: setEnv });
}
}).addTo(app)
app.route({
path: 'npm',
key: 'get',
description: '获取 .npmrc 内容',
metadata: {
args: {}
}
}).define(async () => {
const execPath = process.cwd();
const npmrcPath = path.resolve(execPath, '.npmrc');
if (fileIsExist(npmrcPath)) {
const npmrcContent = fs.readFileSync(npmrcPath, 'utf-8');
console.log(npmrcContent);
}
}).addTo(app)
app.route({
path: 'npm',
key: 'set',
description: '设置 .npmrc',
metadata: {
args: {
force: z.boolean().optional().describe('强制覆盖'),
}
}
}).define(async (ctx) => {
const { force } = ctx.args;
const config = getConfig();
const npmrcContent =
config?.npmrc ||
`/npm.cnb.cool/kevisual/registry/packages/:_authToken=\${CNB_API_KEY}
//registry.npmjs.org/:_authToken=\${NPM_TOKEN}
`;
const execPath = process.cwd();
const npmrcPath = path.resolve(execPath, '.npmrc');
let writeFlag = false;
if (fileIsExist(npmrcPath)) {
if (force) {
writeFlag = true;
} else {
const confirmed = await confirm({
message: `Are you sure you want to overwrite the .npmrc file?`,
default: false,
});
if (confirmed) {
writeFlag = true;
}
}
} else {
writeFlag = true;
}
if (writeFlag) {
fs.writeFileSync(npmrcPath, npmrcContent);
console.log(chalk.green('write .npmrc success'));
}
}).addTo(app)
app.route({
path: 'npm',
key: 'remove',
description: '删除 .npmrc',
metadata: {
args: {}
}
}).define(async () => {
const execPath = process.cwd();
const npmrcPath = path.resolve(execPath, '.npmrc');
if (fileIsExist(npmrcPath)) {
fs.unlinkSync(npmrcPath);
console.log(chalk.green('remove .npmrc success'));
} else {
console.log(chalk.green('.npmrc already removed'));
}
}).addTo(app)
app.route({
path: 'npm',
key: 'install',
description: 'npm install 使用代理',
metadata: {
args: {
noproxy: z.boolean().optional().describe('不使用代理'),
}
}
}).define(async (ctx) => {
const { noproxy } = ctx.args;
const cwd = process.cwd();
const config = getConfig();
let setEnv: any = {};
const proxyEnv = {
https_proxy: 'http://127.0.0.1:7890',
http_proxy: 'http://127.0.0.1:7890',
all_proxy: 'socks5://127.0.0.1:7890',
...config?.proxy,
};
setEnv = { ...proxyEnv };
if (noproxy) {
setEnv = {};
}
if (checkPnpm()) {
spawn('pnpm', ['i'], { stdio: 'inherit', cwd, env: { ...process.env, ...setEnv } });
} else {
spawn('npm', ['i'], { stdio: 'inherit', cwd, env: { ...process.env, ...setEnv } });
}
}).addTo(app)
app.route({
path: 'npm',
key: 'patch',
description: 'npm patch 发布补丁版本',
metadata: {
args: {}
}
}).define(async () => {
patchFunc();
}).addTo(app)

35
src/routes/proxy.ts Normal file
View File

@@ -0,0 +1,35 @@
import { app } from '../app.ts';
import { z } from 'zod';
import { chalk } from '@/module/chalk.ts';
app.route({
path: 'proxy',
key: 'main',
description: '执行代理相关的命令',
metadata: {
args: {
start: z.boolean().optional().describe('启动代理'),
unset: z.boolean().optional().describe('关闭代理'),
}
}
}).define(async (ctx) => {
const { start, unset } = ctx.args;
const proxyShell = 'export https_proxy=http://127.0.0.1:7890 http_proxy=http://127.0.0.1:7890 all_proxy=socks5://127.0.0.1:7890';
const unProxyShell = 'unset https_proxy http_proxy all_proxy';
if (start) {
console.log(chalk.green('启动代理'));
console.log(chalk.green('执行以下命令以启用代理:'));
console.log(`\n ${chalk.yellow(proxyShell)}\n`);
console.log(`请运行以下命令应用代理:`);
console.log(chalk.cyan(`eval "$(${process.argv[1]} proxy -s)"`));
} else if (unset) {
console.log(chalk.green('关闭代理'));
console.log(chalk.green('执行以下命令以禁用代理:'));
console.log(`\n ${chalk.yellow(unProxyShell)}\n`);
console.log(`请运行以下命令取消代理:`);
console.log(chalk.cyan(`eval "$(${process.argv[1]} proxy -u)"`));
} else {
console.log(chalk.red('请提供选项 -s 或 -u'));
}
}).addTo(app)

395
src/routes/publish.ts Normal file
View File

@@ -0,0 +1,395 @@
import { app } from '../app.ts';
import { z } from 'zod';
import fs from 'fs';
import path from 'path';
import glob from 'fast-glob';
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 { input } from '@inquirer/prompts';
import { logger } from '@/module/logger.ts';
async function findFileInsensitive(targetFile: string): Promise<string | null> {
const files = fs.readdirSync('.');
const matchedFile = files.find((file) => file.toLowerCase() === targetFile.toLowerCase());
return matchedFile || null;
}
async function collectFileInfo(filePath: string, baseDir = '.'): Promise<any[]> {
const stats = fs.statSync(filePath);
const relativePath = path.relative(baseDir, filePath);
if (stats.isFile()) {
return [{ path: relativePath, size: stats.size }];
}
if (stats.isDirectory()) {
const files = fs.readdirSync(filePath);
const results = await Promise.all(files.map((file) => collectFileInfo(path.join(filePath, file), baseDir)));
return results.flat();
}
return [];
}
export const copyFilesToPackDist = async (files: string[], cwd: string, packDist = 'pack-dist', mergeDist = true) => {
const packDistPath = path.join(cwd, packDist);
if (!fileIsExist(packDistPath)) {
fs.mkdirSync(packDistPath, { recursive: true });
} else {
fs.rmSync(packDistPath, { recursive: true, force: true });
}
files.forEach((file) => {
const stat = fs.statSync(path.join(cwd, file));
let outputFile = file;
if (mergeDist) {
if (file.startsWith('dist/')) {
outputFile = file.replace(/^dist\//, '');
} else if (file === 'dist') {
outputFile = '';
}
}
if (stat.isDirectory()) {
fs.cpSync(path.join(cwd, file), path.join(packDistPath, outputFile), { recursive: true });
} else {
fs.copyFileSync(path.join(cwd, file), path.join(packDistPath, outputFile));
}
});
const packageInfo = await getPackageInfo();
const indexHtmlPath = path.join(packDistPath, 'index.html');
const collectionFiles = (await Promise.all(files.map((file) => collectFileInfo(file)))).flat();
const prettifySize = (size: number) => {
if (size < 1024) return `${size}B`;
if (size < 1024 * 1024) return `${(size / 1024).toFixed(2)}kB`;
return `${(size / 1024 / 1024).toFixed(2)}MB`;
};
const filesString = collectionFiles.map((file) => `<li><a href="${file.path}">${file.path}</a><span>${prettifySize(file.size)}</span></li>`).join('\n');
const indexHtmlContent = `<!DOCTYPE html>
<html>
<head><title>${packageInfo.name}</title></head>
<body>
<h1>${packageInfo.name}</h1>
<ul>${filesString}</ul>
<pre>${JSON.stringify(packageInfo, null, 2)}</pre>
</body>
</html>`;
if (!fileIsExist(indexHtmlPath)) {
fs.writeFileSync(indexHtmlPath, indexHtmlContent);
}
};
export const pack = async (opts: { packDist?: string, mergeDist?: boolean }) => {
const cwd = process.cwd();
const collection: Record<string, any> = {};
const mergeDist = opts.mergeDist !== false;
const packageJsonPath = path.join(cwd, 'package.json');
if (!fileIsExist(packageJsonPath)) {
console.error('package.json not found');
return;
}
let packageJson;
try {
const packageContent = fs.readFileSync(packageJsonPath, 'utf-8');
packageJson = JSON.parse(packageContent);
} catch (error) {
console.error('Invalid package.json:', error);
return;
}
let files = packageJson.files;
const filesToInclude = files
? await glob(files, {
cwd: cwd,
dot: true,
onlyFiles: false,
followSymbolicLinks: true,
ignore: ['node_modules/**', ".git/**", opts.packDist ? opts.packDist + '/**' : ''],
})
: [];
const readmeFile = await findFileInsensitive('README.md');
if (readmeFile && !filesToInclude.includes(readmeFile)) {
filesToInclude.push(readmeFile);
}
const packageFile = await findFileInsensitive('package.json');
if (packageFile && !filesToInclude.includes(packageFile)) {
filesToInclude.push(packageFile);
}
const allFiles = (await Promise.all(filesToInclude.map((file) => collectFileInfo(file)))).flat();
logger.debug('文件列表:');
allFiles.forEach((file) => logger.debug(`${file.size}B ${file.path}`));
const totalSize = allFiles.reduce((sum, file) => sum + file.size, 0);
collection.files = allFiles;
collection.packageJson = packageJson;
collection.totalSize = totalSize;
collection.tags = packageJson.app?.tags || packageJson.keywords || [];
logger.debug('\n基本信息');
logger.debug(`name: ${packageJson.name}`);
logger.debug(`version: ${packageJson.version}`);
logger.debug(`total files: ${allFiles.length}`);
try {
copyFilesToPackDist(filesToInclude, cwd, opts.packDist, mergeDist);
} catch (error) {
console.error('Error creating tarball:', error);
}
const readme = await findFileInsensitive('README.md');
if (readme) {
const readmeContent = fs.readFileSync(readme, 'utf-8');
collection.readme = readmeContent;
}
return { collection, dir: cwd };
};
export const getPackageInfo = async () => {
const cwd = process.cwd();
const packageJsonPath = path.join(cwd, 'package.json');
try {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
return packageJson;
} catch (error) {
console.error('Invalid package.json:', error);
return {};
}
};
const deployLoadFn = async (id: string, fileKey: string, force = true, install = false) => {
if (!id) {
console.error(chalk.red('id is required'));
return;
}
let appKey = '';
let version = '';
if (id && id.includes('/')) {
const [a, b] = id.split('/');
if (a) {
appKey = b || '1.0.0';
version = a;
id = '';
} else {
console.error(chalk.red('id format error, please use "version/appKey" format'));
return;
}
}
const res = await query.post({
path: 'micro-app',
key: 'deploy',
data: { id, version, appKey, key: fileKey, force, install: !!install },
});
if (res.code === 200) {
console.log('deploy-load success. current version:', res.data?.pkg?.version);
console.log('run: ', 'envision services -s', res.data?.showAppInfo?.key);
} else {
console.error('deploy-load 失败', res.message);
}
return res;
};
app.route({
path: 'publish',
key: 'main',
description: '发布应用',
metadata: {
args: {
key: z.string().optional().describe('应用 key'),
version: z.string().optional().describe('应用版本'),
}
}
}).define(async (ctx) => {
const { key, version } = ctx.args;
const config = await getConfig();
console.log('发布逻辑实现', { key, version, config });
}).addTo(app)
app.route({
path: 'pack',
key: 'main',
description: '打包应用',
metadata: {
args: {
publish: z.boolean().optional().describe('打包并发布'),
update: z.boolean().optional().describe('发布后显示更新命令'),
packDist: z.string().optional().describe('打包到的目录'),
mergeDist: z.boolean().optional().describe('合并 dist 目录到 pack-dist 中'),
yes: z.boolean().optional().describe('确定,直接打包'),
clean: z.boolean().optional().describe('清理 package.json中的 devDependencies'),
org: z.string().optional().describe('org'),
}
}
}).define(async (ctx) => {
const { publish, update, packDist, mergeDist, yes, clean, org } = ctx.args;
const dist = packDist || 'pack-dist';
const shouldMergeDist = mergeDist !== false;
const shouldYes = yes !== false;
const packageInfo = await getPackageInfo();
if (!packageInfo) {
console.error('Invalid package.json:');
return;
}
let basename = packageInfo.basename || '';
let appKey: string | undefined;
let version = packageInfo.version || '';
if (!version) {
version = await input({ message: 'Enter your version:' });
}
if (basename) {
if (basename.startsWith('/')) {
basename = basename.slice(1);
}
const basenameArr = basename.split('/');
if (basenameArr.length !== 2) {
console.error(chalk.red('basename is error, 请输入正确的路径, packages.json中basename例如 root/appKey'));
return;
}
appKey = basenameArr[1] || '';
}
if (!appKey) {
appKey = await input({ message: 'Enter your appKey:' });
}
await pack({ packDist: dist, mergeDist: shouldMergeDist });
if (clean) {
const newPackageJson = { ...packageInfo };
delete newPackageJson.devDependencies;
fs.writeFileSync(path.join(process.cwd(), 'pack-dist', 'package.json'), JSON.stringify(newPackageJson, null, 2));
}
}).addTo(app)
app.route({
path: 'pack-deploy',
key: 'main',
description: 'Pack 部署',
metadata: {
args: {
id: z.string().describe('id'),
key: z.string().optional().describe('fileKey'),
install: z.boolean().optional().describe('install dependencies'),
}
}
}).define(async (ctx) => {
const { id, key, install } = ctx.args;
await deployLoadFn(id, key, true, install);
}).addTo(app)
app.route({
path: 'services',
key: 'main',
description: '服务器服务管理',
metadata: {
args: {
list: z.boolean().optional().describe('list services'),
restart: z.string().optional().describe('restart services'),
start: z.string().optional().describe('start services'),
stop: z.string().optional().describe('stop services'),
info: z.string().optional().describe('info services'),
delete: z.string().optional().describe('delete services'),
}
}
}).define(async (ctx) => {
const { list, restart, start, stop, info, delete: del } = ctx.args;
if (list) {
const res = await backServices.queryServiceList();
if (res.code === 200) {
const data = res.data as any[];
console.log('services list');
const getMaxLengths = (data: any[]) => {
const lengths = { key: 0, status: 0, type: 0, description: 0, version: 0 };
data.forEach((item) => {
lengths.key = Math.max(lengths.key, item.key.length);
lengths.status = Math.max(lengths.status, item.status.length);
lengths.type = Math.max(lengths.type, item.type.length);
lengths.description = Math.max(lengths.description, item.description.length);
lengths.version = Math.max(lengths.version, item.version.length);
});
return lengths;
};
const lengths = getMaxLengths(data);
const padString = (str: string, length: number) => str + ' '.repeat(Math.max(length - str.length, 0));
console.log(
chalk.blue(padString('Key', lengths.key)),
chalk.green(padString('Status', lengths.status)),
chalk.yellow(padString('Type', lengths.type)),
chalk.red(padString('Version', lengths.version)),
);
data.forEach((item) => {
console.log(
chalk.blue(padString(item.key, lengths.key)),
chalk.green(padString(item.status, lengths.status)),
chalk.blue(padString(item.type, lengths.type)),
chalk.green(padString(item.version, lengths.version)),
);
});
} else {
console.log('error', chalk.red(res.message || '获取列表失败'));
}
return;
}
if (restart) {
const res = await backServices.queryServiceOperate(restart, 'restart');
if (res.code === 200) {
console.log('restart success');
} else {
console.error('restart failed', res.message);
}
return;
}
if (start) {
const res = await backServices.queryServiceOperate(start, 'start');
if (res.code === 200) {
console.log('start success');
} else {
console.error('start failed', res.message);
}
return;
}
if (stop) {
const res = await backServices.queryServiceOperate(stop, 'stop');
if (res.code === 200) {
console.log('stop success');
} else {
console.log(chalk.red('stop failed'), res.message);
}
return;
}
if (info) {
const res = await backServices.queryServiceList();
if (res.code === 200) {
const data = res.data as any[];
const item = data.find((item) => item.key === info);
if (!item) {
console.log('not found');
return;
}
console.log(chalk.blue(item.key), chalk.green(item.status), chalk.yellow(item.type), chalk.red(item.version));
console.log('description:', chalk.blue(item.description));
} else {
console.log(chalk.red(res.message || '获取列表失败'));
}
}
if (del) {
const res = await backServices.queryServiceDelect(del);
if (res.code === 200) {
console.log('delete success');
} else {
console.log(chalk.red('delete failed'), res.message);
}
}
}).addTo(app)
app.route({
path: 'services',
key: 'detect',
description: '检测服务',
metadata: {
args: {}
}
}).define(async () => {
const res = await backServices.queryServiceDetect();
console.log('detect', res);
}).addTo(app)

110
src/routes/remote-config.ts Normal file
View File

@@ -0,0 +1,110 @@
import { app } from '../app.ts';
import { z } from 'zod';
import { query } from '@/module/query.ts';
import { QueryConfig } from '@kevisual/api/query-config';
import { showMore } from '@/uitls/show-more.ts';
import fs from 'node:fs';
import path from 'node:path';
const queryConfig = new QueryConfig({ query: query as any });
app.route({
path: 'remote-config',
key: 'get',
alias: 'rc',
description: '获取远程配置',
metadata: {
args: {
key: z.string().optional().describe('配置键名'),
}
}
}).define(async (ctx) => {
const { key } = ctx.args;
if (!key) {
console.log('Please provide a key using -k or --key option.');
return;
}
const res = await queryConfig.getConfigByKey(key);
console.log('res Config Result:', showMore(res.data));
}).addTo(app)
app.route({
path: 'remote-config',
key: 'list',
description: '列出所有远程配置',
metadata: {
args: {}
}
}).define(async () => {
const res = await queryConfig.listConfig();
console.log('Remote Configs:', res);
if (res.code === 200) {
const list = res.data?.list || [];
list.forEach((item: any) => {
console.log(item.id, item.key, item.data);
});
}
}).addTo(app)
app.route({
path: 'remote-config',
key: 'update',
description: '更新远程配置',
metadata: {
args: {
key: z.string().describe('配置键名'),
value: z.string().optional().describe('配置值'),
file: z.string().optional().describe('从文件读取配置值'),
}
}
}).define(async (ctx) => {
const { key, value, file } = ctx.args;
if (!key) {
console.log('请提供配置键名,使用 -k 或 --key 选项。', ctx.args);
return;
}
try {
let data: any = {}
const filePath = path.resolve(process.cwd(), file || '');
const hasFile = fs.existsSync(filePath);
if (value) {
data = JSON.parse(value);
} else if (file || hasFile) {
if (!hasFile) {
console.log('指定的文件不存在:', filePath);
return;
}
data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
} else {
console.log('请提供配置值,使用 -v 或 --value 选项,或使用 -f 或 --file 从文件读取。');
return;
}
const res = await queryConfig.updateConfig({
key,
data,
});
console.log('Update Config Result:', showMore(res.data));
} catch (error) {
console.log('Error parsing JSON:');
}
}).addTo(app)
app.route({
path: 'remote-config',
key: 'delete',
description: '删除远程配置',
metadata: {
args: {
id: z.string().optional().describe('配置ID'),
key: z.string().optional().describe('配置键名'),
}
}
}).define(async (ctx) => {
const { key, id } = ctx.args;
if (!key && !id) {
console.log('请提供配置键名或配置ID使用 -k 或 --key 选项,或 -i 或 --id 选项。');
return;
}
const res = await queryConfig.deleteConfig({ key, id });
console.log('Delete Config Result:', showMore(res));
}).addTo(app)

View File

@@ -0,0 +1,95 @@
import { app } from '../app.ts';
import { z } from 'zod';
import { query } from '@/module/query.ts';
import { QueryConfig } from '@kevisual/api/query-secret';
import { showMore } from '@/uitls/show-more.ts';
import fs from 'node:fs';
import path from 'node:path';
const queryConfig = new QueryConfig({ query: query as any });
app.route({
path: 'remote-secret',
key: 'get',
alias: 'rs',
description: '获取远程密钥',
metadata: {
args: {
key: z.string().optional().describe('配置键名'),
}
}
}).define(async (ctx) => {
const { key } = ctx.args;
if (!key) {
console.log('Please provide a key using -k or --key option.');
return;
}
const res = await queryConfig.getItem({ id: key });
console.log('res Config Result:', showMore(res.data));
}).addTo(app)
app.route({
path: 'remote-secret',
key: 'list',
description: '列出所有密钥',
metadata: {
args: {}
}
}).define(async () => {
const res = await queryConfig.listItems();
if (res.code === 200) {
const list = res.data?.list || [];
list.forEach((item: any) => {
console.log(item.id, item.key, showMore(item));
});
} else {
console.log('获取错误:', res.message);
}
}).addTo(app)
app.route({
path: 'remote-secret',
key: 'update',
description: '更新密钥',
metadata: {
args: {
id: z.string().optional().describe('配置ID'),
title: z.string().optional().describe('配置值'),
description: z.string().optional().describe('配置数据JSON格式'),
}
}
}).define(async (ctx) => {
const { id, title, description } = ctx.args;
let updateData: any = {};
if (title) {
updateData.title = title;
}
if (description) {
updateData.description = description;
}
if (id) {
updateData.id = id;
}
const res = await queryConfig.updateItem(updateData);
console.log('修改结果:', showMore(res));
}).addTo(app)
app.route({
path: 'remote-secret',
key: 'delete',
description: '删除密钥',
metadata: {
args: {
id: z.string().optional().describe('配置ID'),
key: z.string().optional().describe('配置键名'),
}
}
}).define(async (ctx) => {
const { key, id } = ctx.args;
if (!key && !id) {
console.log('请提供配置键名或配置ID使用 -k 或 --key 选项,或 -i 或 --id 选项。');
return;
}
const res = await queryConfig.deleteItem({ key, id });
console.log('Delete Config Result:', showMore(res));
}).addTo(app)

47
src/routes/router.ts Normal file
View File

@@ -0,0 +1,47 @@
import { app } from '../app.ts';
import { z } from 'zod';
import { input } from '@inquirer/prompts';
import { query } from '@/module/query.ts';
import chalk from 'chalk';
import util from 'util';
app.route({
path: 'router',
key: 'service',
description: 'router services get',
metadata: {
args: {
path: z.string().optional().describe('第一路径 path'),
key: z.string().optional().describe('第二路径 key'),
}
}
}).define(async (ctx) => {
let { path, key } = ctx.args;
// 如果没有传递参数,则通过交互式输入
if (!path) {
path = await input({
message: 'Enter your path:',
});
}
if (!key) {
key = await input({
message: 'Enter your key:',
});
}
const res = await query.post({ path, key });
if (res?.code === 200) {
console.log('query success');
const _list = res.data?.list || res.data;
if (Array.isArray(_list)) {
const data = _list.map((item: any) => {
return {
id: item.id,
title: item.title,
};
});
console.log(chalk.green(util.inspect(data, { colors: true, depth: 4 })));
}
} else {
console.log('error', res.message || '');
}
}).addTo(app)

317
src/routes/sync.ts Normal file
View File

@@ -0,0 +1,317 @@
import { app } from '../app.ts';
import { z } from 'zod';
import { SyncBase } from '@/command/sync/modules/base.ts';
import { baseURL, query, storage } from '@/module/query.ts';
import { fetchLink, fetchAiList } from '@/module/download/install.ts';
import fs from 'node:fs';
import { upload } from '@/module/download/upload.ts';
import { logger, printClickableLink } from '@/module/logger.ts';
import { chalk } from '@/module/chalk.ts';
import path from 'node:path';
import { fileIsExist } from '@/uitls/file.ts';
import { confirm } from '@inquirer/prompts'
app.route({
path: 'sync',
key: 'main',
description: '同步项目',
metadata: {
args: {
dir: z.string().optional().describe('配置目录'),
}
}
}).define(async () => {
console.log('同步项目');
}).addTo(app)
app.route({
path: 'sync',
key: 'upload',
description: '上传项目',
metadata: {
args: {
dir: z.string().optional().describe('配置目录'),
config: z.string().optional().describe('配置文件的名字'),
file: z.string().optional().describe('操作的对应的文件名'),
list: z.boolean().optional().describe('显示上传列表,不上传文件'),
}
}
}).define(async (ctx) => {
const { dir, config, file, list } = ctx.args;
console.log('上传项目');
const isUpload = list ? false : true;
const sync = new SyncBase({ dir, baseURL, configFilename: config || 'kevisual.json' });
const syncList = await sync.getSyncList({ getFile: true });
logger.debug(syncList);
const nodonwArr: any[] = [];
const token = storage.getItem('token');
const meta: Record<string, string> = {
...sync.config.metadata,
};
const filepath = sync.getRelativePath(file);
const newInfos = [];
const uploadLength = syncList.length;
logger.info(`开始上传文件,总计 ${uploadLength} 个文件`);
if (uploadLength > 100) {
const shouldContinue = await confirm({
message: `即将上传 ${uploadLength} 个文件,是否继续?`,
default: false,
});
if (!shouldContinue) {
logger.info('已取消上传');
return;
}
}
for (const item of syncList) {
if (!item.auth || !item.exist) {
nodonwArr.push(item);
continue;
}
if (!sync.canDone(item.type, 'upload')) {
nodonwArr.push(item);
continue;
}
if (filepath && item.filepath !== filepath.absolute) {
continue;
}
if (!isUpload) {
console.log('上传列表', item.key, chalk.green(item.url));
continue
}
const res = await upload({
token,
file: fs.readFileSync(item.filepath),
url: item.url,
needHash: true,
hash: item.hash,
meta: item.metadata ?? meta,
});
if (res.code === 200) {
if (res.data?.isNew) {
newInfos.push(['上传成功', item.key, chalk.green(item.url), chalk.green('文件上传')]);
} else if (res.data?.isNewMeta) {
newInfos.push(['上传成功', item.key, chalk.green(item.url), chalk.green('元数据更新')]);
} else {
logger.debug('上传成功', item.key, chalk.green(item.url), chalk.blue('文件未更新'));
}
}
logger.debug(res);
}
if (newInfos.length) {
logger.info('上传成功的文件\n');
newInfos.forEach((item) => {
logger.info(...item);
});
}
if (nodonwArr.length && !filepath) {
logger.warn('以下文件未上传\n', nodonwArr.map((item) => item.key).join(','));
}
}).addTo(app)
app.route({
path: 'sync',
key: 'download',
description: '下载项目',
metadata: {
args: {
dir: z.string().optional().describe('配置目录'),
config: z.string().optional().describe('配置文件的名字'),
file: z.string().optional().describe('操作的对应的文件名'),
}
}
}).define(async (ctx) => {
const { dir, config, file } = ctx.args;
const sync = new SyncBase({ dir, baseURL, configFilename: config || 'kevisual.json' });
const syncList = await sync.getSyncList();
logger.debug(syncList);
const nodonwArr: any[] = [];
const filepath = sync.getRelativePath(file);
for (const item of syncList) {
if (!sync.canDone(item.type, 'download')) {
nodonwArr.push(item);
continue;
}
if (filepath && item.filepath !== filepath.absolute) {
continue;
}
const hash = sync.getHash(item.filepath);
const { content, status } = await fetchLink(item.url, { setToken: item.auth, returnContent: true, hash });
if (status === 200) {
await sync.getDir(item.filepath, true);
fs.writeFileSync(item.filepath, content);
logger.info('下载成功', item.key, chalk.green(item.url));
} else if (status === 304) {
logger.info('文件未修改', item.key, chalk.green(item.url));
} else {
logger.error('下载失败', item.key, chalk.red(item.url));
}
}
if (nodonwArr.length && !filepath) {
logger.warn('以下文件未下载', nodonwArr.map((item) => item.key).join(','));
}
}).addTo(app)
app.route({
path: 'sync',
key: 'list',
description: '列出同步列表',
metadata: {
args: {
dir: z.string().optional().describe('配置目录'),
config: z.string().optional().describe('配置文件的名字'),
all: z.boolean().optional().describe('显示所有的文件'),
local: z.boolean().optional().describe('显示本地的文件列表'),
}
}
}).define(async (ctx) => {
const { dir, config, all, local } = ctx.args;
const sync = new SyncBase({ dir, baseURL, configFilename: config || 'kevisual.json' });
const getLocalFile = local ? true : false;
const syncList = await sync.getSyncList({ getFile: true, getLocalFile });
logger.debug(syncList);
logger.info('同步列表\n');
syncList.forEach((item) => {
if (all) {
logger.info(item);
} else {
logger.info(chalk.green(printClickableLink({ url: item.url, text: item.key, print: false })), chalk.gray(item.type));
}
});
}).addTo(app)
app.route({
path: 'sync',
key: 'create',
description: '创建同步配置',
metadata: {
args: {
dir: z.string().optional().describe('配置目录'),
config: z.string().optional().describe('配置文件的名字'),
output: z.string().optional().describe('输出文件'),
}
}
}).define(async (ctx) => {
const { dir, config, output } = ctx.args;
const sync = new SyncBase({ dir, baseURL, configFilename: config || 'kevisual.json' });
const syncList = await sync.getSyncList();
logger.debug(syncList);
logger.info('同步列表\n');
let newSync: any = {};
syncList.forEach((item) => {
logger.info(chalk.blue(item.key), chalk.gray(item.type), chalk.green(item.url));
newSync[item.key] = item.url;
});
const newJson = { ...sync.config };
newJson.sync = newSync;
const filepath = sync.getRelativePath(output);
if (filepath) {
logger.debug('输出文件', filepath);
fs.writeFileSync(filepath.absolute, JSON.stringify(newJson, null, 2));
} else {
logger.info('输出内容\n');
logger.info(newJson);
}
}).addTo(app)
app.route({
path: 'sync',
key: 'clone',
description: '克隆同步目录',
metadata: {
args: {
dir: z.string().optional().describe('配置目录'),
config: z.string().optional().describe('配置文件的名字'),
link: z.string().optional().describe('克隆链接'),
local: z.boolean().optional().describe('只对sync列表clone'),
}
}
}).define(async (ctx) => {
const { dir, config, link, local } = ctx.args;
let cloneLink = link || '';
const isLocal = local || false;
const sync = new SyncBase({ dir, baseURL, configFilename: config || 'kevisual.json' });
if (cloneLink) {
if (!cloneLink.endsWith('.json')) {
cloneLink = cloneLink + (cloneLink.endsWith('/') ? '' : '/') + 'kevisual.json';
}
const res = await query.fetchText(cloneLink);
if (res.code === 200) {
fs.writeFileSync(sync.configPath, JSON.stringify(res.data, null, 2));
} else {
logger.error('下载配置文件失败', cloneLink, res);
return;
}
await sync.init()
}
const syncList = await sync.getSyncList({ getLocalFile: !isLocal });
logger.debug(syncList);
logger.info('检查目录\n');
const checkList = await sync.getCheckList();
logger.info('检查列表', checkList);
for (const item of checkList) {
if (!item.auth) {
continue;
}
if (!item.enabled) {
logger.info('提示:', item.key, chalk.yellow('未启用'));
continue;
}
const res = await fetchAiList(item.url, { recursive: true });
if (res.code === 200) {
const data = res?.data || [];
let matchObjectList = data.filter((dataItem) => {
dataItem.pathname = path.join(item.key || '', dataItem.path);
return dataItem;
});
matchObjectList = sync.getMatchList({ ignore: item.ignore, matchObjectList }).matchObjectList;
const matchList = matchObjectList
.map((item2: any) => {
const rp = sync.getRelativePath(item2.pathname);
if (!rp) return false;
return { ...item2, relative: rp.relative, absolute: rp.absolute };
})
.filter((i: any) => i);
for (const matchItem of matchList) {
if (!matchItem) continue;
if (isLocal) {
const some = syncList.some((syncItem) => {
if (syncItem.url === matchItem.url) {
return true;
}
return false;
});
if (!some) {
continue;
}
}
let needDownload = true;
let hash = '';
await sync.getDir(matchItem.absolute, true);
logger.debug('文件路径', matchItem.absolute);
if (fileIsExist(matchItem.absolute)) {
hash = sync.getHash(matchItem.absolute);
if (hash !== matchItem.etag) {
logger.error('文件不一致', matchItem.pathname, chalk.red(matchItem.url), chalk.red('文件不一致'));
} else {
needDownload = false;
logger.info('文件一致', matchItem.pathname, chalk.green(matchItem.url), chalk.green('文件一致'));
}
}
if (needDownload) {
const { content, status } = await fetchLink(matchItem.url, { setToken: item.auth, returnContent: true, hash });
if (status === 200) {
fs.writeFileSync(matchItem.absolute, content);
logger.info('下载成功', matchItem.pathname, chalk.green(matchItem.url));
} else if (status === 304) {
logger.info('文件未修改', matchItem.pathname, chalk.green(matchItem.url));
} else {
logger.error('下载失败', matchItem.pathname, chalk.red(matchItem.url));
}
}
}
} else {
logger.error('检查失败', item.url, res.code);
break;
}
}
}).addTo(app)

119
src/routes/update.ts Normal file
View File

@@ -0,0 +1,119 @@
import { app } from '../app.ts';
import { z } from 'zod';
import { execSync } from 'node:child_process';
import path from 'node:path';
import fs from 'node:fs';
import { getConfig } from '@/module/get-config.ts';
import { fetchLink } from '@/module/download/install.ts';
import { fileIsExist } from '@/uitls/file.ts';
import { getHash, getBufferHash } from '@/uitls/hash.ts';
import { useContextKey } from '@kevisual/context'
import semver from 'semver'
const getRunFilePath = () => {
const c = process.argv[1];
const runFilePath = path.resolve(c);
const isJs = runFilePath.endsWith('.js');
let distDir = '';
if (isJs) {
const dir = path.dirname(runFilePath);
distDir = path.relative(dir, '../dist');
} else {
distDir = path.resolve(process.cwd(), 'dist');
}
return distDir;
}
const distFiles = ["assistant-server.js", "assistant.js", "envision.js"];
const downloadNewDistFiles = async (distDir: string) => {
const baseURL = getConfig().baseURL || 'https://kevisual.cn';
const newData = distFiles.map(file => {
const url = `${baseURL}/root/cli/dist/${file}`;
const filePath = path.join(distDir, file);
const exist = fileIsExist(filePath);
let hash = '';
hash = getHash(filePath);
return { url, filePath, exist, hash };
});
const promises = newData.map(async ({ url, filePath }) => {
return await fetchLink(url, { returnContent: true });
});
let isUpdate = false;
await Promise.all(promises).then(results => {
results.forEach((res, index) => {
const data = newData[index];
const filePath = data.filePath;
const newHash = getBufferHash(res.content);
if (data.hash === newHash) {
return;
}
console.log('更新文件:', filePath);
isUpdate = true;
if (data.exist) {
fs.writeFileSync(filePath, res.content, 'utf-8');
} else {
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(filePath, res.content, 'utf-8');
}
});
if (isUpdate) {
console.log('更新完成,请重新运行命令');
} else {
console.log('检测完成');
}
}).catch(error => {
console.error('Error downloading files:', error);
});
}
const getVersion = async (force?: boolean) => {
const runFilePath = getRunFilePath();
if (force) {
await downloadNewDistFiles(runFilePath);
return;
}
const baseURL = getConfig().baseURL || 'https://kevisual.cn';
const file = 'package.json';
const url = `${baseURL}/root/cli/${file}`;
const res = await fetchLink(url, { returnContent: true });
const text = res.content.toString('utf-8');
const json = JSON.parse(text);
const latestVersion = json.version;
const version = useContextKey('version');
if (semver.lt(version, latestVersion)) {
console.log('当前版本:', version, '最新版本:', latestVersion, '正在更新...');
downloadNewDistFiles(runFilePath);
} else {
console.log('已经是最新版本', version);
}
}
app.route({
path: 'update',
key: 'main',
description: 'update cli',
metadata: {
args: {
global: z.boolean().optional().describe('update global'),
npm: z.boolean().optional().describe('use npm to update'),
force: z.boolean().optional().describe('force update'),
}
}
}).define(async (ctx) => {
const { global, npm, force } = ctx.args;
try {
if (npm) {
const cmd = global ? 'npm install -g @kevisual/envision-cli' : 'npm install -D @kevisual/envision-cli';
execSync(cmd, { stdio: 'inherit', encoding: 'utf-8' });
} else {
const forceUpdate = force ? true : false;
await getVersion(forceUpdate);
}
} catch (error) {
console.error('Error updating CLI:', error);
}
}).addTo(app)