Auto commit: 2026-03-24 13:04
This commit is contained in:
45
src/routes/ai.ts
Normal file
45
src/routes/ai.ts
Normal 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
153
src/routes/app.ts
Normal 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
148
src/routes/cc.ts
Normal 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
140
src/routes/ccc.ts
Normal 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
82
src/routes/cnb.ts
Normal 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
228
src/routes/config.ts
Normal 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
250
src/routes/deploy.ts
Normal 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
90
src/routes/docker.ts
Normal 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
103
src/routes/download.ts
Normal 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
82
src/routes/gist.ts
Normal 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
79
src/routes/jwks.ts
Normal 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
267
src/routes/npm.ts
Normal 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
35
src/routes/proxy.ts
Normal 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
395
src/routes/publish.ts
Normal 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
110
src/routes/remote-config.ts
Normal 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)
|
||||
95
src/routes/remote-secret.ts
Normal file
95
src/routes/remote-secret.ts
Normal 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
47
src/routes/router.ts
Normal 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
317
src/routes/sync.ts
Normal 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
119
src/routes/update.ts
Normal 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)
|
||||
Reference in New Issue
Block a user