Compare commits

...

8 Commits

23 changed files with 901 additions and 506 deletions

4
.gitignore vendored
View File

@@ -8,4 +8,6 @@ pack-dist
assistant-app
build
.pnpm-store
.pnpm-store
jwt

2
apps/.gitignore vendored
View File

@@ -1,2 +0,0 @@
container
root

View File

@@ -1,13 +0,0 @@
{
"name": "apps",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@kevisual/assistant-cli",
"version": "0.0.7",
"version": "0.0.8",
"description": "",
"main": "dist/assistant.mjs",
"keywords": [
@@ -41,20 +41,19 @@
}
},
"devDependencies": {
"@kevisual/ai": "^0.0.21",
"@kevisual/api": "^0.0.22",
"@kevisual/ai": "^0.0.22",
"@kevisual/api": "^0.0.26",
"@kevisual/load": "^0.0.6",
"@kevisual/local-app-manager": "^0.1.32",
"@kevisual/logger": "^0.0.4",
"@kevisual/query": "0.0.37",
"@kevisual/query": "0.0.38",
"@kevisual/query-login": "0.0.7",
"@kevisual/router": "^0.0.60",
"@kevisual/router": "^0.0.62",
"@kevisual/types": "^0.0.12",
"@kevisual/use-config": "^1.0.28",
"@opencode-ai/plugin": "^1.1.28",
"@opencode-ai/plugin": "^1.1.36",
"@types/bun": "^1.3.6",
"@types/lodash-es": "^4.17.12",
"@types/node": "^25.0.9",
"@types/node": "^25.0.10",
"@types/send": "^1.2.1",
"@types/ws": "^8.18.1",
"chalk": "^5.6.2",
@@ -64,7 +63,6 @@
"dotenv": "^17.2.3",
"get-port": "^7.1.0",
"inquirer": "^13.2.1",
"lodash-es": "^4.17.22",
"nanoid": "^5.1.6",
"send": "^1.2.1",
"supports-color": "^10.2.2",
@@ -78,11 +76,12 @@
"access": "public"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.972.0",
"@aws-sdk/client-s3": "^3.975.0",
"@kevisual/ha-api": "^0.0.6",
"@kevisual/js-filter": "^0.0.5",
"@kevisual/oss": "^0.0.16",
"@kevisual/video-tools": "^0.0.13",
"@opencode-ai/sdk": "^1.1.28",
"@opencode-ai/sdk": "^1.1.36",
"es-toolkit": "^1.44.0",
"eventemitter3": "^5.0.4",
"lowdb": "^7.0.1",

View File

@@ -53,7 +53,7 @@ app.route({
description: '获取路由列表',
}).define(async (ctx) => {
const list = ctx.app.getList((item) => {
if (item.id === 'auth') return false;
if (item?.path?.includes('auth') || item?.id?.includes('auth')) return false;
return true;
})
ctx.body = { list }

View File

@@ -5,5 +5,5 @@ import './routes/index.ts';
import './routes-simple/index.ts';
export const AgentPlugin: Plugin = createRouterAgentPluginFn({
router: app.router,
router: app,
})

View File

@@ -98,10 +98,12 @@ export type AssistantConfigData = {
* Router代理, 会自动获取 {path: 'router', key: 'list'}的路由信息,然后注入到整个router应用当中.
* 例子: { proxy: [ { type: 'router', api: 'https://localhost:50002/api/router' } ] }
* base: 是否使用 /api/router的基础路径默认false
* lightcode: 是否启用lightcode路由默认false
*/
router?: {
proxy: ProxyInfo[];
base?: boolean;
lightcode?: boolean;
}
/**
* API 代理配置, 比如api开头的v1开头的等等

View File

@@ -10,6 +10,7 @@ import { logger } from '@/module/logger.ts';
import { getEnvToken } from '@/module/http-token.ts';
import { initApi } from '@kevisual/api/proxy'
import { Query } from '@kevisual/query';
import { initLightCode } from '@/module/light-code/index.ts';
export class AssistantApp extends Manager {
config: AssistantConfig;
pagesPath: string;
@@ -99,7 +100,9 @@ export class AssistantApp extends Manager {
if (isConnect) {
remoteApp.listenProxy();
this.remoteIsConnected = true;
remoteApp.emitter.once('close', () => {
// 清理已有的 close 事件监听器,防止多重绑定
remoteApp.emitter.removeAllListeners('close');
remoteApp.emitter.on('close', () => {
setTimeout(() => {
if (remoteApp.isError) {
console.error('远程应用发生错误,不重连');
@@ -111,7 +114,7 @@ export class AssistantApp extends Manager {
logger.debug('链接到了远程应用服务器');
const appId = id;
const username = config?.auth.username || 'unknown';
const url = new URL(`/${username}/v1/${appId}`, 'https://kevisual.cn/');
const url = new URL(`/${username}/v1/${appId}`, config?.registry || 'https://kevisual.cn/');
this.remoteUrl = url.toString();
console.log('远程地址', this.remoteUrl);
} else {
@@ -129,6 +132,7 @@ export class AssistantApp extends Manager {
const config = this.config.getConfig();
const routerProxy = config?.router?.proxy || [];
const base = config.router?.base ?? false;
const lightcode = config.router?.lightcode ?? true;
if (base) {
routerProxy.push({
type: 'router',
@@ -137,12 +141,27 @@ export class AssistantApp extends Manager {
}
})
}
if (lightcode) {
routerProxy.push({
type: 'lightcode',
lightcode: {
check: true,
}
})
}
if (routerProxy.length === 0) {
return
}
for (const proxyInfo of routerProxy) {
if (proxyInfo.type !== 'router') {
console.warn('路由的type必须是"router"');
if (proxyInfo.type !== 'router' && proxyInfo.type !== 'lightcode') {
console.warn('路由的type必须是"router", 或者lightcode');
continue;
}
if (proxyInfo.type === 'lightcode') {
initLightCode({
router: this.mainApp,
config: this.config
});
continue;
}
const url = proxyInfo.router!.url;
@@ -173,6 +192,10 @@ export class AssistantApp extends Manager {
console.log('重新连接到远程应用服务器...', this.attemptedConnectTimes);
const remoteApp = this.remoteApp;;
if (remoteApp) {
// 先关闭旧的 WebSocket防止竞态条件
if (remoteApp.ws) {
remoteApp.ws.close();
}
remoteApp.init();
this.attemptedConnectTimes += 1;
const isConnect = await remoteApp.isConnect();
@@ -180,8 +203,8 @@ export class AssistantApp extends Manager {
remoteApp.listenProxy();
this.attemptedConnectTimes = 0;
console.log('重新连接到了远程应用服务器');
this.reconnectRemoteApp();
} else {
this.reconnectRemoteApp();
setTimeout(() => {
this.initRouterApp()
}, 30 * 1000 + this.attemptedConnectTimes * 10 * 1000); // 30秒后重连 + 每次增加10秒

View File

@@ -14,7 +14,7 @@ export type ProxyInfo = {
/**
* 类型
*/
type?: 'file' | 'dynamic' | 'minio' | 'http' | 's3' | 'router';
type?: 'file' | 'dynamic' | 'minio' | 'http' | 's3' | 'router' | 'lightcode';
/**
* 目标的 pathname 默认为请求的url.pathname, 设置了pathname则会使用pathname作为请求的url.pathname
* @default undefined
@@ -45,6 +45,13 @@ export type ProxyInfo = {
router?: {
id?: string;
url?: string;
},
lightcode?: {
id?: string;
/**
* 是否检测远程服务更新
*/
check?: boolean;
}
};

View File

@@ -0,0 +1,16 @@
import crypto from 'node:crypto';
import fs from 'node:fs';
export const getHash = (file: string) => {
if (!fs.existsSync(file)) return '';
const buffer = fs.readFileSync(file); // 不指定编码,返回 Buffer
return crypto.createHash('md5').update(buffer).digest('hex');
};
export const getBufferHash = (buffer: Buffer) => {
return crypto.createHash('md5').update(buffer).digest('hex');
};
export const getStringHash = (str: string) => {
return crypto.createHash('md5').update(str).digest('hex');
}

View File

@@ -0,0 +1,177 @@
import { App, QueryRouterServer } from '@kevisual/router';
import { AssistantInit } from '../../services/init/index.ts';
import path from 'node:path';
import fs, { write } from 'node:fs';
import os from 'node:os';
import glob from 'fast-glob';
import { runCode } from './run.ts';
const codeDemoId = '0e700dc8-90dd-41b7-91dd-336ea51de3d2'
import { filter } from "@kevisual/js-filter";
import { getHash, getStringHash } from '../file-hash.ts';
import { AssistantConfig } from '@/lib.ts';
const codeDemo = `// 这是一个示例代码文件
import {App} from '@kevisual/router';
const app = new App();
app.route({
path: 'hello',
description: 'LightCode 示例路由',
metadata: {
tags: ['light-code', 'example'],
},
}).define(async (ctx) => {
console.log('tokenUser:', ctx.query?.tokenUser);
ctx.body = 'Hello from LightCode!';
}).addTo(app);
app.wait();
`;
const writeCodeDemo = async (appDir: string) => {
const lightcodeDir = path.join(appDir, 'light-code', 'code');
const demoPath = path.join(lightcodeDir, `${codeDemoId}.ts`);
fs.writeFileSync(demoPath, codeDemo, 'utf-8');
}
// writeCodeDemo(path.join(os.homedir(), 'kevisual', 'assistant-app', 'apps'));
type opts = {
router: QueryRouterServer | App
config: AssistantConfig | AssistantInit
sync?: boolean
}
type LightCodeFile = {
id?: string, code?: string, hash?: string, filepath: string
}
export const initLightCode = async (opts: opts) => {
// 注册 light-code 路由
console.log('初始化 light-code 路由');
const config = opts.config as AssistantInit;
const app = opts.router;
const token = config.getConfig()?.token || '';
const query = config.query;
const sync = opts.sync ?? true;
if (!config || !app) {
console.error('initLightCode 缺少必要参数, config 或 app');
return;
}
const appDir = config.configPath.appsDir;
const lightcodeDir = path.join(appDir, 'light-code', 'code');
if (!fs.existsSync(lightcodeDir)) {
fs.mkdirSync(lightcodeDir, { recursive: true });
}
let diffList: LightCodeFile[] = [];
const codeFiles = glob.sync(['**/*.ts', '**/*.js'], {
cwd: lightcodeDir,
onlyFiles: true,
}).map(file => {
return {
filepath: path.join(lightcodeDir, file),
// hash: getHash(path.join(lightcodeDir, file))
}
});
if (sync) {
const queryRes = await query.post({
path: 'light-code',
key: 'list',
token,
});
if (queryRes.code === 200) {
const lightQueryList = queryRes.data?.list || [];
for (const item of lightQueryList) {
const codeHash = getStringHash(item.code || '');
diffList.push({ id: item.id!, code: item.code || '', hash: codeHash, filepath: path.join(lightcodeDir, `${item.id}.ts`) });
}
const codeFileSet = new Set(codeFiles.map(f => f.filepath));
// 需要新增的文件 (在 diffList 中但不在 codeFiles 中)
const toAdd = diffList.filter(d => !codeFileSet.has(d.filepath));
// 需要删除的文件 (在 codeFiles 中但不在 diffList 中)
const toDelete = codeFiles.filter(f => !diffList.some(d => d.filepath === f.filepath));
// 需要更新的文件 (两边都有但 hash 不同)
const toUpdate = diffList.filter(d => codeFileSet.has(d.filepath) && d.hash !== getHash(d.filepath));
const unchanged = diffList.filter(d => codeFileSet.has(d.filepath) && d.hash === getHash(d.filepath));
// 执行新增
for (const item of toAdd) {
fs.writeFileSync(item.filepath, item.code, 'utf-8');
// console.log(`新增 light-code 文件: ${item.filepath}`);
}
// 执行删除
for (const filepath of toDelete) {
fs.unlinkSync(filepath.filepath);
// console.log(`删除 light-code 文件: ${filepath.filepath}`);
}
// 执行更新
for (const item of toUpdate) {
fs.writeFileSync(item.filepath, item.code, 'utf-8');
// console.log(`更新 light-code 文件: ${item.filepath}`);
}
// 记录未更新的文件
// const lightCodeList = [...toAdd, ...unchanged].map(d => ({
// filepath: d.filepath,
// hash: d.hash
// }));
} else {
console.error('light-code 同步失败', queryRes.message);
diffList = codeFiles;
}
} else {
diffList = codeFiles;
}
for (const file of diffList) {
const tsPath = file.filepath;
const runRes = await runCode(tsPath, { path: 'router', key: 'list' }, { timeout: 10000 });
if (runRes.success) {
const res = runRes.data;
if (res.code === 200) {
const list = res.data?.list || [];
for (const routerItem of list) {
if (routerItem.path?.includes('auth') || routerItem.path?.includes('router') || routerItem.path?.includes('call')) {
continue;
}
// console.log(`注册 light-code 路由: [${routerItem.path}] ${routerItem.id} 来自文件: ${file.filepath}`);
const metadata = routerItem.metadata || {};
if (metadata.tags && Array.isArray(metadata.tags)) {
metadata.tags.push('light-code');
} else {
metadata.tags = ['light-code'];
}
app.route({
id: routerItem.id,
path: `${routerItem.id}__${routerItem.path}`,
key: routerItem.key,
description: routerItem.description || '',
metadata,
middleware: ['auth'],
}).define(async (ctx) => {
const tokenUser = ctx.state?.tokenUser || {};
const query = { ...ctx.query, tokenUser }
const runRes2 = await runCode(tsPath, query, { timeout: 30000 });
if (runRes2.success) {
const res2 = runRes2.data;
if (res2.code === 200) {
ctx.body = res2.data;
} else {
ctx.throw(res2.code, res2.message || 'Lightcode 路由执行失败');
}
} else {
ctx.throw(runRes2.error || 'Lightcode 路由执行失败');
}
}).addTo(app);
}
}
} else {
console.error('light-code 路由执行失败', runRes.error);
}
}
console.log(`light-code 路由注册成功`, `注册${diffList.length}个路由`);
}

View File

@@ -32,7 +32,7 @@ type RunCode = {
};
error?: any
timestamp?: string
[key: string]: any
output?: string
}
export const runCode = async (tsPath: string, params: RunCodeParams = {}, opts?: RunCodeOptions): Promise<RunCode> => {
return new Promise((resolve, reject) => {
@@ -43,7 +43,7 @@ export const runCode = async (tsPath: string, params: RunCodeParams = {}, opts?:
})
return
}
let output = ''
const timeoutMs = opts?.timeout || 30000; // 默认30秒超时
let child
@@ -70,6 +70,7 @@ export const runCode = async (tsPath: string, params: RunCodeParams = {}, opts?:
if (!resolved) {
resolved = true
cleanup()
result.output = output
resolve(result)
}
}
@@ -77,7 +78,11 @@ export const runCode = async (tsPath: string, params: RunCodeParams = {}, opts?:
try {
// 使用 Bun 的 fork 模式启动子进程
child = fork(tsPath, [], {
silent: true // 启用 stdio 重定向
silent: true, // 启用 stdio 重定向
env: {
...process.env,
BUN_CHILD_PROCESS: 'true' // 标记为子进程
}
})
// 监听来自子进程的消息
child.on('message', (msg: RunCode) => {
@@ -94,6 +99,11 @@ export const runCode = async (tsPath: string, params: RunCodeParams = {}, opts?:
})
})
}
if (child.stdout) {
child.stdout.on('data', (data) => {
output += data.toString()
})
}
// 监听子进程退出事件
child.on('exit', (code, signal) => {

View File

@@ -1,7 +1,7 @@
import type { App } from '@kevisual/router';
import { loadAppInfo, AppInfoConfig, saveAppInfo, getAppsPath } from './app-file.ts';
import { fork } from 'node:child_process';
import { merge } from 'lodash-es';
import { merge } from 'es-toolkit';
import { deleteFileAppInfo } from './app-file.ts';
import { fileIsExist } from '@kevisual/use-config/env';
import path from 'node:path';

View File

@@ -64,6 +64,10 @@ export class RemoteApp {
throw new Error('No id provided for remote app');
}
this.isError = false;
// 关闭已有连接
if (this.ws) {
this.ws.close();
}
const ws = new WebSocket(this.getWsURL(this.url));
const that = this;
ws.onopen = function () {

View File

@@ -71,7 +71,7 @@ program
.option('-n, --name <name>', '服务名称', 'assistant-server')
.option('-p, --port <port>', '服务端口')
.option('-s, --start', '是否启动服务')
.option('-r, --root <root>', '工作空间路径')
.option('-r, --root <root>', 'assistant app的根目录路径')
.option('-i, --input <input>', '启动的输入文件,例如/workspace/src/main.ts')
.option('-e, --interpreter <interpreter>', '指定使用的解释器', 'bun')
.action(async (options) => {

View File

@@ -136,30 +136,30 @@ export class AssistantInit extends AssistantConfig {
"author": "",
"license": "ISC",
"dependencies": {
"@aws-sdk/client-s3": "latest",
"@kevisual/oss": "latest",
"@kevisual/query": "latest",
"@kevisual/router": "latest",
"@kevisual/use-config": "latest",
"better-sqlite3": "latest",
"crypto-js": "latest",
"dayjs": "latest",
"dotenv": "latest",
"es-toolkit": "latest",
"eventemitter3": "latest",
"ioredis": "latest",
"minio": "latest",
"node-cron": "latest",
"pg": "latest",
"pm2": "latest",
"sequelize": "latest",
"unstorage": "latest"
"@aws-sdk/client-s3": "^3.975.0",
"@kevisual/oss": "^0.0.16",
"@kevisual/query": "^0.0.38",
"eventemitter3": "^5.0.4",
"@kevisual/router": "^0.0.62",
"@kevisual/use-config": "^1.0.28",
"ioredis": "^5.9.2",
"minio": "^8.0.6",
"pg": "^8.17.2",
"pm2": "^6.0.14",
"sequelize": "^6.37.7",
"crypto-js": "^4.2.0",
"better-sqlite3": "^12.6.2",
"unstorage": "^1.17.4",
"dayjs": "^1.11.19",
"es-toolkit": "^1.44.0",
"node-cron": "^4.2.1",
"dotenv": "^17.2.3"
},
"devDependencies": {
"@kevisual/types": "^0.0.11",
"@kevisual/types": "^0.0.12",
"@types/bun": "^1.3.6",
"@types/crypto-js": "latest",
"@types/node": "latest"
"@types/crypto-js": "^4.2.2",
"@types/node": "^25.0.10"
}
}
`,

View File

@@ -12,29 +12,29 @@
"author": "",
"license": "ISC",
"dependencies": {
"@aws-sdk/client-s3": "latest",
"@kevisual/oss": "latest",
"@kevisual/query": "latest",
"@kevisual/router": "latest",
"@kevisual/use-config": "latest",
"better-sqlite3": "latest",
"crypto-js": "latest",
"dayjs": "latest",
"dotenv": "latest",
"es-toolkit": "latest",
"eventemitter3": "latest",
"ioredis": "latest",
"minio": "latest",
"node-cron": "latest",
"pg": "latest",
"pm2": "latest",
"sequelize": "latest",
"unstorage": "latest"
"@aws-sdk/client-s3": "^3.975.0",
"@kevisual/oss": "^0.0.16",
"@kevisual/query": "^0.0.38",
"eventemitter3": "^5.0.4",
"@kevisual/router": "^0.0.62",
"@kevisual/use-config": "^1.0.28",
"ioredis": "^5.9.2",
"minio": "^8.0.6",
"pg": "^8.17.2",
"pm2": "^6.0.14",
"sequelize": "^6.37.7",
"crypto-js": "^4.2.0",
"better-sqlite3": "^12.6.2",
"unstorage": "^1.17.4",
"dayjs": "^1.11.19",
"es-toolkit": "^1.44.0",
"node-cron": "^4.2.1",
"dotenv": "^17.2.3"
},
"devDependencies": {
"@kevisual/types": "^0.0.11",
"@kevisual/types": "^0.0.12",
"@types/bun": "^1.3.6",
"@types/crypto-js": "latest",
"@types/node": "latest"
"@types/crypto-js": "^4.2.2",
"@types/node": "^25.0.10"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@kevisual/cli",
"version": "0.0.92",
"version": "0.0.94",
"description": "envision 命令行工具",
"type": "module",
"basename": "/root/cli",
@@ -46,16 +46,18 @@
"dependencies": {
"@inquirer/prompts": "^8.2.0",
"@kevisual/app": "^0.0.2",
"@kevisual/auth": "^2.0.3",
"@kevisual/context": "^0.0.4",
"@kevisual/use-config": "^1.0.28",
"@opencode-ai/sdk": "^1.1.28",
"@opencode-ai/sdk": "^1.1.36",
"@types/busboy": "^1.5.4",
"busboy": "^1.6.0",
"eventemitter3": "^5.0.4",
"jose": "^6.1.3",
"lowdb": "^7.0.1",
"pm2": "latest",
"lru-cache": "^11.2.4",
"micromatch": "^4.0.8",
"pm2": "latest",
"semver": "^7.7.3",
"unstorage": "^1.17.4"
},
@@ -63,13 +65,13 @@
"@kevisual/dts": "^0.0.3",
"@kevisual/load": "^0.0.6",
"@kevisual/logger": "^0.0.4",
"@kevisual/query": "0.0.37",
"@kevisual/query": "0.0.38",
"@kevisual/query-login": "0.0.7",
"@types/bun": "^1.3.6",
"@types/crypto-js": "^4.2.2",
"@types/jsonwebtoken": "^9.0.10",
"@types/micromatch": "^4.0.10",
"@types/node": "^25.0.9",
"@types/node": "^25.0.10",
"@types/semver": "^7.7.1",
"chalk": "^5.6.2",
"commander": "^14.0.2",
@@ -89,4 +91,4 @@
"publishConfig": {
"access": "public"
}
}
}

897
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -171,7 +171,7 @@ const uploadFiles = async (files: string[], directory: string, opts: UploadFileO
logger.error('请检查文件是否存在');
}
data.files.push({ path: file, hash: hash });
if(filePath.includes('readme.md')) {
if (filePath.includes('readme.md')) {
description = fs.readFileSync(filePath, 'utf-8');
}
}
@@ -215,8 +215,10 @@ const uploadFiles = async (files: string[], directory: string, opts: UploadFileO
}
const filename = path.basename(filePath);
logger.debug('upload file', file, filename);
// 解决 busbox 文件名乱码: 将 UTF-8 编码的文件名转换为 binary 字符串
const encodedFilename = Buffer.from(filename, 'utf-8').toString('binary');
form.append('file', fs.createReadStream(filePath), {
filename: filename,
filename: encodedFilename,
filepath: file,
});
needUpload = true;

74
src/command/jwks.ts Normal file
View File

@@ -0,0 +1,74 @@
import { generate } from '@kevisual/auth'
import { program, Command } from '@/program.ts';
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,
}
}
const jwksCmd = new Command('jwks')
.description('JWKS 相关命令')
.action(async (opts) => {
});
const jwksGenerate = new Command('generate')
.alias('gen')
.option('-d , --dir <dir>', '指定保存目录,默认当前目录下 jwt 文件夹', 'jwt')
.description('生成 JWKS 密钥对')
.action(async (opts) => {
const dir = path.isAbsolute(opts.dir) ? opts.dir : path.join(process.cwd(), opts.dir);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const { JWKS_PATH, PRIVATE_JWK_PATH, PRIVATE_KEY_PATH, PUBLIC_KEY_PATH } = await getPath(dir);
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: ${dir}`);
});
jwksCmd.addCommand(jwksGenerate);
const getJWKS = new Command('get')
.description('获取 JWKS 内容')
.option('-d , --dir <dir>', '指定 JWKS 所在目录,默认当前目录下 jwt 文件夹', 'jwt')
.option('-t, --type <type>', '指定获取类型jwks 或 privateJWK', 'jwks')
.action(async (opts) => {
const dir = path.isAbsolute(opts.dir) ? opts.dir : path.join(process.cwd(), opts.dir);
const { JWKS_PATH, PRIVATE_JWK_PATH } = await getPath(dir);
const type = opts.type || 'jwks';
if (type !== 'jwks') {
if (!fs.existsSync(PRIVATE_JWK_PATH)) {
console.error(`Private JWK file not found in directory: ${dir}`);
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: ${dir}`);
return;
}
const jwksContent = fs.readFileSync(JWKS_PATH, 'utf-8');
console.log('PublicJWKS:\n');
console.log(jwksContent);
});
jwksCmd.addCommand(getJWKS);
program.addCommand(jwksCmd);

View File

@@ -6,8 +6,9 @@ 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 { select, input } from '@inquirer/prompts';
import { input } from '@inquirer/prompts';
import { logger } from '@/module/logger.ts';
// 查找文件(忽略大小写)
async function findFileInsensitive(targetFile: string): Promise<string | null> {
const files = fs.readdirSync('.');
@@ -249,14 +250,9 @@ const packCommand = new Command('pack')
let appKey: string | undefined;
let version = packageInfo.version || '';
if (!version) {
const answers = await inquirer.prompt([
{
type: 'input',
name: 'version',
message: 'Enter your version:',
},
]);
version = answers.version || version;
version = await input({
message: 'Enter your version:',
});
}
if (basename) {
@@ -271,14 +267,9 @@ const packCommand = new Command('pack')
appKey = basenameArr[1] || '';
}
if (!appKey) {
const answers = await inquirer.prompt([
{
type: 'input',
name: 'appKey',
message: 'Enter your appKey:',
},
]);
appKey = answers.appKey || appKey;
appKey = await input({
message: 'Enter your appKey:',
});
}
let value = await pack({
packDist,

View File

@@ -19,6 +19,8 @@ import './command/config-secret-remote.ts';
import './command/ai.ts';
import './command/claude/cc.ts'
import './command/docker.ts';
import './command/jwks.ts';
// program.parse(process.argv);
export const runParser = async (argv: string[]) => {