From b05f059aeadef53ec37b35aa9557ec3aff843a24 Mon Sep 17 00:00:00 2001 From: xiongxiao Date: Tue, 24 Mar 2026 13:04:47 +0800 Subject: [PATCH] Auto commit: 2026-03-24 13:04 --- src/routes/ai.ts | 45 ++++ src/routes/app.ts | 153 ++++++++++++++ src/routes/cc.ts | 148 ++++++++++++++ src/routes/ccc.ts | 140 +++++++++++++ src/routes/cnb.ts | 82 ++++++++ src/routes/config.ts | 228 +++++++++++++++++++++ src/routes/deploy.ts | 250 +++++++++++++++++++++++ src/routes/docker.ts | 90 ++++++++ src/routes/download.ts | 103 ++++++++++ src/routes/gist.ts | 82 ++++++++ src/routes/jwks.ts | 79 ++++++++ src/routes/npm.ts | 267 ++++++++++++++++++++++++ src/routes/proxy.ts | 35 ++++ src/routes/publish.ts | 395 ++++++++++++++++++++++++++++++++++++ src/routes/remote-config.ts | 110 ++++++++++ src/routes/remote-secret.ts | 95 +++++++++ src/routes/router.ts | 47 +++++ src/routes/sync.ts | 317 +++++++++++++++++++++++++++++ src/routes/update.ts | 119 +++++++++++ 19 files changed, 2785 insertions(+) create mode 100644 src/routes/ai.ts create mode 100644 src/routes/app.ts create mode 100644 src/routes/cc.ts create mode 100644 src/routes/ccc.ts create mode 100644 src/routes/cnb.ts create mode 100644 src/routes/config.ts create mode 100644 src/routes/deploy.ts create mode 100644 src/routes/docker.ts create mode 100644 src/routes/download.ts create mode 100644 src/routes/gist.ts create mode 100644 src/routes/jwks.ts create mode 100644 src/routes/npm.ts create mode 100644 src/routes/proxy.ts create mode 100644 src/routes/publish.ts create mode 100644 src/routes/remote-config.ts create mode 100644 src/routes/remote-secret.ts create mode 100644 src/routes/router.ts create mode 100644 src/routes/sync.ts create mode 100644 src/routes/update.ts diff --git a/src/routes/ai.ts b/src/routes/ai.ts new file mode 100644 index 0000000..f49c976 --- /dev/null +++ b/src/routes/ai.ts @@ -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) diff --git a/src/routes/app.ts b/src/routes/app.ts new file mode 100644 index 0000000..821ad1a --- /dev/null +++ b/src/routes/app.ts @@ -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) diff --git a/src/routes/cc.ts b/src/routes/cc.ts new file mode 100644 index 0000000..380a04a --- /dev/null +++ b/src/routes/cc.ts @@ -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 object> = { + minimax: changeMinimax, + glm: changeGLM, + volcengine: changeVolcengine, + bailian: changeBailian, +}; + +const readOrCreateConfig = (configPath: string): Record => { + 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) => { + 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) diff --git a/src/routes/ccc.ts b/src/routes/ccc.ts new file mode 100644 index 0000000..1574849 --- /dev/null +++ b/src/routes/ccc.ts @@ -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; + 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; +}; + +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) diff --git a/src/routes/cnb.ts b/src/routes/cnb.ts new file mode 100644 index 0000000..1ddce1f --- /dev/null +++ b/src/routes/cnb.ts @@ -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) diff --git a/src/routes/config.ts b/src/routes/config.ts new file mode 100644 index 0000000..c97e601 --- /dev/null +++ b/src/routes/config.ts @@ -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) diff --git a/src/routes/deploy.ts b/src/routes/deploy.ts new file mode 100644 index 0000000..c5e2f74 --- /dev/null +++ b/src/routes/deploy.ts @@ -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 => { + 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) diff --git a/src/routes/docker.ts b/src/routes/docker.ts new file mode 100644 index 0000000..dfd11bc --- /dev/null +++ b/src/routes/docker.ts @@ -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) diff --git a/src/routes/download.ts b/src/routes/download.ts new file mode 100644 index 0000000..f7bdf36 --- /dev/null +++ b/src/routes/download.ts @@ -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, + }; +}; diff --git a/src/routes/gist.ts b/src/routes/gist.ts new file mode 100644 index 0000000..a2949a6 --- /dev/null +++ b/src/routes/gist.ts @@ -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) diff --git a/src/routes/jwks.ts b/src/routes/jwks.ts new file mode 100644 index 0000000..8087135 --- /dev/null +++ b/src/routes/jwks.ts @@ -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) diff --git a/src/routes/npm.ts b/src/routes/npm.ts new file mode 100644 index 0000000..b9744b0 --- /dev/null +++ b/src/routes/npm.ts @@ -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) diff --git a/src/routes/proxy.ts b/src/routes/proxy.ts new file mode 100644 index 0000000..47dc20d --- /dev/null +++ b/src/routes/proxy.ts @@ -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) diff --git a/src/routes/publish.ts b/src/routes/publish.ts new file mode 100644 index 0000000..ae27036 --- /dev/null +++ b/src/routes/publish.ts @@ -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 { + const files = fs.readdirSync('.'); + const matchedFile = files.find((file) => file.toLowerCase() === targetFile.toLowerCase()); + return matchedFile || null; +} + +async function collectFileInfo(filePath: string, baseDir = '.'): Promise { + 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) => `
  • ${file.path}${prettifySize(file.size)}
  • `).join('\n'); + const indexHtmlContent = ` + +${packageInfo.name} + +

    ${packageInfo.name}

    +
      ${filesString}
    +
    ${JSON.stringify(packageInfo, null, 2)}
    + +`; + if (!fileIsExist(indexHtmlPath)) { + fs.writeFileSync(indexHtmlPath, indexHtmlContent); + } +}; + +export const pack = async (opts: { packDist?: string, mergeDist?: boolean }) => { + const cwd = process.cwd(); + const collection: Record = {}; + 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) diff --git a/src/routes/remote-config.ts b/src/routes/remote-config.ts new file mode 100644 index 0000000..0084a2e --- /dev/null +++ b/src/routes/remote-config.ts @@ -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) diff --git a/src/routes/remote-secret.ts b/src/routes/remote-secret.ts new file mode 100644 index 0000000..a0a3d72 --- /dev/null +++ b/src/routes/remote-secret.ts @@ -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) diff --git a/src/routes/router.ts b/src/routes/router.ts new file mode 100644 index 0000000..5dc6381 --- /dev/null +++ b/src/routes/router.ts @@ -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) diff --git a/src/routes/sync.ts b/src/routes/sync.ts new file mode 100644 index 0000000..a16adf9 --- /dev/null +++ b/src/routes/sync.ts @@ -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 = { + ...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) diff --git a/src/routes/update.ts b/src/routes/update.ts new file mode 100644 index 0000000..8439330 --- /dev/null +++ b/src/routes/update.ts @@ -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)