diff --git a/bun.config.mjs b/bun.config.ts similarity index 90% rename from bun.config.mjs rename to bun.config.ts index 32523bb..2640c74 100644 --- a/bun.config.mjs +++ b/bun.config.ts @@ -7,7 +7,7 @@ const external = ['bun']; await Bun.build({ target: 'node', format: 'esm', - entrypoints: ['./src/index.ts'], + entrypoints: ['./src/oldindex.ts'], outdir: './dist', naming: { entry: 'envision.js', diff --git a/package.json b/package.json index 20fd991..795d8c1 100644 --- a/package.json +++ b/package.json @@ -26,12 +26,12 @@ "files": [ "dist", "bin", - "bun.config.mjs" + "bun.config.ts" ], "scripts": { - "dev": "bun src/run.ts ", + "dev": "bun src/cli.ts ", "dev:server": "cd assistant && bun --watch src/run-server.ts ", - "build": "rimraf dist && bun run bun.config.mjs", + "build": "rimraf dist && bun run bun.config.ts", "deploy": "ev pack -u -p -m no", "postbuild": "cd assistant && pnpm build " }, @@ -58,7 +58,8 @@ "nanoid": "^5.1.7", "pm2": "latest", "semver": "^7.7.4", - "unstorage": "^1.17.4" + "unstorage": "^1.17.4", + "zod": "^4.3.6" }, "devDependencies": { "@kevisual/api": "^0.0.65", @@ -76,13 +77,14 @@ "chalk": "^5.6.2", "commander": "^14.0.3", "crypto-js": "^4.2.0", + "es-toolkit": "^1.45.1", "fast-glob": "^3.3.3", "filesize": "^11.0.13", "form-data": "^4.0.5", "ignore": "^7.0.5", "jsonwebtoken": "^9.0.3", "pm2": "^6.0.14", - "tar": "^7.5.12", + "tar": "^7.5.13", "zustand": "^5.0.12" }, "engines": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 11c7101..a6294e0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,6 +62,9 @@ importers: unstorage: specifier: ^1.17.4 version: 1.17.4(idb-keyval@6.2.2) + zod: + specifier: ^4.3.6 + version: 4.3.6 devDependencies: '@kevisual/api': specifier: ^0.0.65 @@ -108,6 +111,9 @@ importers: crypto-js: specifier: ^4.2.0 version: 4.2.0 + es-toolkit: + specifier: ^1.45.1 + version: 1.45.1 fast-glob: specifier: ^3.3.3 version: 3.3.3 @@ -124,8 +130,8 @@ importers: specifier: ^9.0.3 version: 9.0.3 tar: - specifier: ^7.5.12 - version: 7.5.12 + specifier: ^7.5.13 + version: 7.5.13 zustand: specifier: ^5.0.12 version: 5.0.12(react@19.2.4) @@ -2257,8 +2263,8 @@ packages: resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} engines: {node: '>=10.0.0'} - tar@7.5.12: - resolution: {integrity: sha512-9TsuLcdhOn4XztcQqhNyq1KOwOOED/3k58JAvtULiYqbO8B/0IBAAIE1hj0Svmm58k27TmcigyDI0deMlgG3uw==} + tar@7.5.13: + resolution: {integrity: sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==} engines: {node: '>=18'} to-regex-range@5.0.1: @@ -4928,7 +4934,7 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 - tar@7.5.12: + tar@7.5.13: dependencies: '@isaacs/fs-minipass': 4.0.1 chownr: 3.0.0 diff --git a/src/ai/ai.ts b/src/ai/ai.ts deleted file mode 100644 index 118b349..0000000 --- a/src/ai/ai.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { App } from '@kevisual/app/mod.ts'; -import { storage } from '../module/query.ts'; -import { sessionStorage } from '../module/cache.ts'; -export const app = new App({ token: storage.getItem('token') || '', storage: sessionStorage }); \ No newline at end of file diff --git a/src/ai/index.ts b/src/ai/index.ts deleted file mode 100644 index c109f88..0000000 --- a/src/ai/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { app } from './ai.ts' -import './routes/cmd-run.ts' - -export { - app -} \ No newline at end of file diff --git a/src/ai/routes/cmd-run.ts b/src/ai/routes/cmd-run.ts deleted file mode 100644 index 68f920f..0000000 --- a/src/ai/routes/cmd-run.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { app } from '../ai.ts'; -import { execSync } from 'node:child_process' -import { logger } from '@/module/logger.ts'; -const promptTemplate = `# CMD 结果判断器 - -分析上一条 CMD 命令的执行结果,判断是否需要执行下一条命令。 - -- 若结果中隐含或明确指示需继续执行 → 返回:\`{"cmd": "推断出的下一条命令", "type": "cmd"}\` -- 若无后续操作,甚至上一次执行的返回为空或者成功 → 返回:\`{"type": "none"}\` - -1. 仅输出合法 JSON,无任何额外文本。 -2. \`cmd\` 必须从执行结果中合理推断得出,非预设或猜测。 -3. 禁止解释、注释、换行或格式错误。` - -app.router.route({ - path: 'cmd-run', - description: '执行 CMD 命令并判断下一步操作, 参数是 cmd 字符串', -}).define(async (ctx) => { - const cmd = ctx.query.cmd || ''; - if (!cmd) { - ctx.throw(400, 'cmd is required'); - } - let result = ''; - ctx.state.steps = ctx.state?.steps || []; - - try { - logger.info('执行命令:', cmd); - result = execSync(cmd, { encoding: 'utf-8' }); - ctx.state.steps.push({ cmd, result }); - logger.info(result); - } catch (error: any) { - result = error.message || ''; - ctx.state.steps.push({ cmd, result, error: true }); - ctx.body = { - steps: ctx.state.steps, - } - return; - } - await app.loadAI() - const prompt = `${promptTemplate}\n上一条命令:\n${cmd}\n执行结果:\n${result}\n`; - const response = await app.ai.question(prompt); - - const msg = app.ai.utils.extractJsonFromMarkdown(app.ai.responseText); - try { - logger.debug('AI Prompt', prompt); - logger.debug('AI 分析结果:', msg); - const { cmd, type } = msg; - if (type === 'cmd' && cmd) { - await app.router.call({ path: 'cmd-run', payload: { cmd } }, { state: ctx.state }); - } else { - logger.info('无后续命令,结束执行'); - ctx.state.steps.push({ type: 'none' }); - } - } catch (error) { - result = '执行错误,无法解析返回结果为合法 JSON' + app.ai.responseText - logger.error(result); - ctx.state.steps.push({ cmd, result, parseError: true }); - } - ctx.body = { - steps: ctx.state.steps, - } -}).addTo(app.router); \ No newline at end of file diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..329ac7a --- /dev/null +++ b/src/app.ts @@ -0,0 +1,6 @@ +import { App } from '@kevisual/router'; +import { useContextKey } from '@kevisual/context'; + +export const app = useContextKey('app', () => { + return new App() +}); \ No newline at end of file diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..161bfdf --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,4 @@ +import { app } from './index.ts'; +import { parse } from "@kevisual/router/commander" + +parse({ app }); \ No newline at end of file diff --git a/src/command/login.ts b/src/command/login.ts index 90eb0fa..980faa3 100644 --- a/src/command/login.ts +++ b/src/command/login.ts @@ -1,6 +1,6 @@ import { program, Command } from '@/program.ts'; import { getConfig, getEnvToken } from '@/module/get-config.ts'; -import { input, password } from '@inquirer/prompts'; +import { input } from '@inquirer/prompts'; import { loginInCommand } from '@/module/login/login-by-web.ts'; import { queryLogin, storage } from '@/module/query.ts'; import chalk from 'chalk'; diff --git a/src/index.ts b/src/index.ts index 8963af8..780817d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,34 +1,7 @@ -import { program } from '@/program.ts'; -import './command/login.ts'; -import './command/ls-token.ts'; -import './command/deploy.ts'; -import './command/config.ts'; -import './command/router.ts'; -import './command/npm.ts'; -import './command/publish.ts'; -import './command/proxy.ts'; -import './command/update.ts'; +import { app } from './app.ts' +import './routes/login.ts' -import './command/sync/sync.ts'; +export { app }; -import './command/app/index.ts'; - -import './command/gist/index.ts'; -import './command/config-remote.ts'; -import './command/config-secret-remote.ts'; -// import './command/ai.ts'; -import './command/coding-plan/cc.ts' -import './command/coding-plan/oc.ts' -import './command/docker.ts'; -import './command/jwks.ts'; - -import './command/cnb/index.ts'; -import './command/download.ts'; - -// program.parse(process.argv); - -export const runParser = async (argv: string[]) => { - // program.parse(process.argv); - // console.log('argv', argv); - program.parse(argv); -}; +app.createAuth(() => { }) +app.createRouteList() \ No newline at end of file diff --git a/src/oldindex.ts b/src/oldindex.ts new file mode 100644 index 0000000..ae2fe78 --- /dev/null +++ b/src/oldindex.ts @@ -0,0 +1,33 @@ +import { program } from '@/program.ts'; +import './command/login.ts'; +import './command/ls-token.ts'; +import './command/deploy.ts'; +import './command/config.ts'; +import './command/router.ts'; +import './command/npm.ts'; +import './command/publish.ts'; +import './command/proxy.ts'; +import './command/update.ts'; + +import './command/sync/sync.ts'; + +import './command/app/index.ts'; + +import './command/gist/index.ts'; +import './command/config-remote.ts'; +import './command/config-secret-remote.ts'; +import './command/coding-plan/cc.ts' +import './command/coding-plan/oc.ts' +import './command/docker.ts'; +import './command/jwks.ts'; + +import './command/cnb/index.ts'; +import './command/download.ts'; + +// program.parse(process.argv); + +export const runParser = async (argv: string[]) => { + // program.parse(process.argv); + // console.log('argv', argv); + program.parse(argv); +}; diff --git a/src/routes/login.ts b/src/routes/login.ts new file mode 100644 index 0000000..b12bffd --- /dev/null +++ b/src/routes/login.ts @@ -0,0 +1,194 @@ +import { app } from '../app.ts'; +import { z } from 'zod'; +import { getConfig, getEnvToken } from '@/module/get-config.ts'; +import { input, password as inputPassword } from '@inquirer/prompts'; +import { loginInCommand } from '@/module/login/login-by-web.ts'; +import { queryLogin, storage } from '@/module/query.ts'; +import chalk from 'chalk'; +import util from 'util'; +import { pick } from 'es-toolkit' + +export const getUsername = async () => { + const token = getEnvToken(); + const localToken = storage.getItem('token'); + if (!token && !localToken) { + console.log('请先登录'); + return null; + } + let me = await queryLogin.getMe(localToken || token); + if (me?.code === 401) { + me = await queryLogin.getMe(); + } + if (me?.code === 200) { + return me.data?.username; + } + return null; +} +const showMe = async (show = true) => { + const token = getEnvToken(); + const localToken = storage.getItem('token'); + if (!token && !localToken) { + console.log('请先登录'); + return { code: 40400, message: '请先登录' }; + } + let me = await queryLogin.getMe(localToken || token); + if (me?.code === 401) { + me = await queryLogin.getMe(); + } + if (show) { + console.log('Me', me.data); + } + return me; +}; + +app.route({ + path: 'user', + key: 'login', + description: '登录', + metadata: { + args: { + username: z.string().optional().describe('用户名'), + password: z.string().optional().describe('密码'), + force: z.boolean().optional().describe('强制登录'), + web: z.boolean().optional().describe('是否通过web登录'), + env: z.boolean().optional().describe('是否通过环境变量KEVISUAL_TOKEN登录'), + } + } +}).define(async (ctx) => { + let { username, password, force, web, env } = ctx.args; + if (web) { + await loginInCommand(); + return; + } + // 从环境变量登录 + if (env) { + const envToken = getEnvToken(); + if (!envToken) { + console.log('环境变量 KEVISUAL_TOKEN 未设置'); + return; + } + const res = await showMe(false); + if (res.code === 200) { + console.log('Login success:', res.data?.username || res.data?.email); + } else { + console.log('Login failed:', res.message || 'Invalid token'); + } + return; + } + // 如果没有传递参数,则通过交互式输入 + if (!username) { + username = await input({ + message: 'Enter your username:', + }); + } + if (!password) { + password = await inputPassword({ + message: 'Enter your password:', + }); + } + const token = storage.getItem('token'); + if (token) { + const res = await showMe(false); + if (res.code === 200) { + const data = res.data; + if (data.username === username) { + console.log('Already Login For', data.username); + return; + } + } + } + + const res = await queryLogin.login({ + username, + password, + }).catch((err) => { + return { code: 500, message: err.message || '' }; + }); + if (res.code === 200) { + console.log('welcome', username); + } else { + console.log('登录失败', res.message || ''); + } +}).addTo(app) + + +app.route({ + path: 'user', + key: 'me', + description: '查看当前登录用户信息', + metadata: { + args: { + all: z.boolean().optional().describe('是否显示全部信息,默认为false'), + } + } +}).define(async (ctx) => { + const options = ctx.args; + try { + let res = await showMe(false); + let isRefresh = false; + if (res.code === 200 && res.data?.accessToken) { + res = await showMe(false); + isRefresh = true; + } + if (res.code === 40400) { + return + } + if (res.code === 200) { + if (isRefresh) { + console.log(chalk.green('refresh token success'), '\n'); + } + } else { + console.log( + isRefresh ? chalk.red('refresh token failed, please login again.') : chalk.red('you need login first. \n run `envision login` to login'), + '\n', + ); + return; + } + const baseURL = getConfig().baseURL; + const pickData = pick(res?.data, ['username', 'type', 'orgs']); + + console.log(chalk.blue('baseURL', baseURL)); + if (options.all) { + console.log(chalk.blue(util.inspect(res?.data, { colors: true, depth: 4 }))); + } else { + // 打印pickData + console.log(chalk.blue(util.inspect(pickData, { colors: true, depth: 4 }))); + } + } catch (error) { + console.log('me error', error); + } +}).addTo(app) + +app.route({ + path: 'user', + key: 'switch', + description: '切换到其他组织或用户', + metadata: { + args: { + username: z.string().describe('用户名或组织名'), + } + } +}).define(async (ctx) => { + const { username } = ctx.args; + const res = await queryLogin.switchUser(username); + if (res.code === 200) { + console.log('success switch to', username); + } else { + console.log('switch to', username, 'failed', res.message || ''); + } +}).addTo(app) + +app.route({ + path: 'user', + key: 'logout', + description: '退出登录', + metadata: {} +}).define(async () => { + try { + await queryLogin.logout(); + storage.removeItem('token'); + console.log('退出成功'); + } catch (error) { + console.log('退出失败', error); + } +}).addTo(app) \ No newline at end of file diff --git a/src/routes/token-ls.ts b/src/routes/token-ls.ts new file mode 100644 index 0000000..2e464e9 --- /dev/null +++ b/src/routes/token-ls.ts @@ -0,0 +1,252 @@ +import { app } from '../app.ts'; +import { z } from 'zod'; +import { getConfig, getEnvToken, writeConfig } from '@/module/get-config.ts'; +import { queryLogin, storage } from '@/module/query.ts'; +import { Kevisual } from '@/module/kevisual.ts'; +import { showMore } from '@/uitls/show-more.ts'; + +function isNumeric(str: string) { + return /^-?\d+\.?\d*$/.test(str); +} + +const showList = (list: string[]) => { + if (list.length === 0) { + console.log('expand baseURLList is empty'); + return; + } + const config = getConfig(); + console.log('----current baseURL:' + config.baseURL + '----\n'); + list.forEach((item, index) => { + console.log(`${index + 1}: ${item}`); + }); +}; + +app.route({ + path: 'token', + key: 'ls', + description: '显示 token 列表', + metadata: { + args: {} + } +}).define(async () => { + console.log('show token list'); + queryLogin.cacheStore.init(); + console.log(queryLogin.cacheStore.cacheData); +}).addTo(app) + +app.route({ + path: 'token', + key: 'info', + description: '显示 token 信息', + metadata: { + args: { + env: z.boolean().optional().describe('显示环境变量中的 token'), + } + } +}).define(async (ctx) => { + const { env } = ctx.args; + const token = storage.getItem('token'); + if (env) { + console.log('token in env', getEnvToken()); + } else { + console.log('token', token); + } +}).addTo(app) + +app.route({ + path: 'token', + key: 'create', + description: '创建 jwks token', + metadata: { + args: {} + } +}).define(async () => { + const kevisual = new Kevisual(); + const res = await kevisual.getAdminToken(); + if (res.code === 200) { + const jwtToken = res.data?.accessToken; + console.log('============jwt token============\n\n'); + console.log(jwtToken); + } else { + console.log('create token failed', showMore(res)); + } +}).addTo(app) + +app.route({ + path: 'baseURL', + key: 'info', + description: '显示 baseURL', + metadata: { + args: { + add: z.string().optional().describe('添加 baseURL'), + remove: z.number().optional().describe('按编号移除 baseURL'), + set: z.union([z.number(), z.string()]).optional().describe('设置 baseURL'), + list: z.boolean().optional().describe('列出 baseURL'), + clear: z.boolean().optional().describe('清除 baseURL'), + } + } +}).define(async (ctx) => { + let { add, remove, set, list, clear } = ctx.args; + let config = getConfig(); + let baseList = (config.baseURLList as Array) || []; + if (!config.baseURL) { + baseList = ['https://kevisual.cn']; + writeConfig({ ...config, baseURL: 'https://kevisual.cn', baseURLList: baseList }); + config = getConfig(); + } + const quineList = (list: string[]) => { + const newList = new Set(list); + return Array.from(newList); + }; + + if (add || set) { + let change = false; + if (add) { + change = true; + baseList.push(add); + } else if (set) { + if (!isNumeric(String(set))) { + change = true; + baseList.push(String(set)); + writeConfig({ ...config, baseURL: String(set) }); + config = getConfig(); + } + } + if (change) { + baseList = quineList(baseList); + writeConfig({ ...config, baseURLList: baseList }); + config = getConfig(); + showList(baseList); + } + } + if (remove !== undefined) { + const index = remove - 1; + if (index < 0 || index >= baseList.length) { + console.log('index out of range'); + return; + } + const removeBase = baseList.splice(index, 1); + baseList = quineList(baseList); + showList(baseList); + writeConfig({ ...config, baseURLList: baseList }); + removeBase[0]; + return; + } + if (set !== undefined) { + const isNumber = isNumeric(String(set)); + if (isNumber) { + const index = Number(set) - 1; + if (index < 0 || index >= baseList.length) { + console.log('index out of range'); + return; + } + writeConfig({ ...config, baseURL: baseList[index] }); + showList(baseList); + } + return; + } + if (list) { + showList(baseList); + return; + } + if (clear) { + writeConfig({ ...config, baseURLList: [] }); + return; + } + if (!config.baseURL) { + config = getConfig(); + writeConfig({ ...config, baseURL: 'https://kevisual.cn' }); + config = getConfig(); + } + console.log('current baseURL:', config.baseURL); +}).addTo(app) + +app.route({ + path: 'baseURL', + key: 'set', + description: '设置 baseURL', + metadata: { + args: { + baseURL: z.string().optional().describe('baseURL 地址'), + } + } +}).define(async (ctx) => { + const config = getConfig(); + let baseURL = ctx.args.baseURL; + if (!baseURL) { + console.log('baseURL is required'); + return; + } + writeConfig({ ...config, baseURL }); +}).addTo(app) + +app.route({ + path: 'registry', + key: 'manage', + description: 'registry 管理', + metadata: { + args: { + list: z.boolean().optional().describe('列出 registry'), + set: z.string().optional().describe('设置 registry'), + } + } +}).define(async (ctx) => { + const { list, set } = ctx.args; + const config = getConfig(); + const defaultRegistry = ['https://kevisual.cn', 'https://kevisual.silkyai.cn', 'https://kevisual.xiongxiao.me', 'http://localhost:3005']; + if (list) { + showList(defaultRegistry); + return; + } + if (set) { + const isNumber = isNumeric(set); + if (isNumber) { + const index = Number(set) - 1; + if (index < 0 || index >= defaultRegistry.length) { + console.log('index out of range'); + return; + } + writeConfig({ ...config, baseURL: defaultRegistry[index] }); + console.log('set registry', defaultRegistry[index]); + } else { + writeConfig({ ...config, baseURL: set }); + console.log('set registry', set); + } + } +}).addTo(app) + +app.route({ + path: 'baseURL', + key: 'kevisual', + description: 'kevisual registry', + metadata: { args: {} } +}).define(async () => { + const config = getConfig(); + const defaultRegistry = ['https://kevisual.cn']; + writeConfig({ ...config, baseURL: defaultRegistry[0] }); + showList(defaultRegistry); +}).addTo(app) + +app.route({ + path: 'baseURL', + key: 'silky', + description: 'silky registry', + metadata: { args: {} } +}).define(async () => { + const config = getConfig(); + const defaultRegistry = ['https://kevisual.silkyai.cn']; + writeConfig({ ...config, baseURL: defaultRegistry[0] }); + showList(defaultRegistry); +}).addTo(app) + +app.route({ + path: 'baseURL', + key: 'local', + description: 'local registry', + metadata: { args: {} } +}).define(async () => { + const config = getConfig(); + const defaultRegistry = ['http://localhost:3005']; + writeConfig({ ...config, baseURL: defaultRegistry[0] }); + showList(defaultRegistry); +}).addTo(app) diff --git a/src/run.ts b/src/run.ts deleted file mode 100644 index f383088..0000000 --- a/src/run.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { runParser } from './index.ts'; - -runParser(process.argv);