fix: update envision-cli tools for proxy and run app

This commit is contained in:
熊潇 2024-12-05 02:26:47 +08:00
parent 2145fca826
commit da6211299b
15 changed files with 738 additions and 479 deletions

View File

@ -1,6 +1,6 @@
{
"name": "@kevisual/envision-cli",
"version": "0.0.6",
"version": "0.0.10",
"description": "envision command tools",
"main": "dist/index.js",
"type": "module",
@ -14,8 +14,7 @@
],
"scripts": {
"dev": "tsx src/run.ts ",
"build": "rimraf dist && rollup -c",
"b": "./bin/envision.js"
"build": "rimraf dist && rollup -c"
},
"keywords": [
"kevisual",
@ -28,6 +27,7 @@
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-replace": "^6.0.1",
"@rollup/plugin-typescript": "^12.1.1",
"@types/node": "^22.10.1",
"chalk": "^5.3.0",
@ -39,7 +39,7 @@
"ignore": "^6.0.2",
"inquirer": "^12.1.0",
"rimraf": "^6.0.1",
"rollup": "^4.27.4",
"rollup": "^4.28.0",
"rollup-plugin-dts": "^6.1.1",
"rollup-plugin-esbuild": "^6.1.1",
"tar": "^7.4.3",
@ -50,16 +50,16 @@
"picomatch": "^4"
},
"engines": {
"node": ">=18.0.0"
"node": ">=22.0.0"
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"@kevisual/router": "^0.0.5",
"@kevisual/router": "^0.0.6-alpha-2",
"pg-hstore": "^2.3.4",
"sequelize": "^6.37.5",
"sqlite3": "^5.1.7",
"vite": "^6.0.1"
"vite": "^6.0.2"
}
}

790
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -4,24 +4,27 @@ import typescript from '@rollup/plugin-typescript';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json';
import path from 'path'
import esbuild from 'rollup-plugin-esbuild'
import alias from '@rollup/plugin-alias'
import path from 'path';
import esbuild from 'rollup-plugin-esbuild';
import alias from '@rollup/plugin-alias';
import replace from '@rollup/plugin-replace';
/**
* @type {import('rollup').RollupOptions}
*/
export default {
const config = {
input: 'src/index.ts', // TypeScript 入口文件
output: {
// file: 'dist/app.js', // 输出文件
dir: 'dist',
entryFileNames: 'app.mjs',
chunkFileNames: '[name]-[hash].mjs',
// format: 'cjs'
format: 'esm', // 输出格式设置为 ES 模块
},
plugins: [
replace({
preventAssignment: true, // 防止意外赋值
VERSION: JSON.stringify('1.0.0'),
}),
alias({
// only esbuild needs to be configured
entries: [
@ -45,8 +48,8 @@ export default {
{ find: 'events', replacement: 'node:events' },
{ find: 'url', replacement: 'node:url' },
{ find: 'assert', replacement: 'node:assert' },
{ find: 'util', replacement: 'node:util' }
]
{ find: 'util', replacement: 'node:util' },
],
}),
resolve({
preferBuiltins: true, // 强制优先使用内置模块
@ -57,7 +60,7 @@ export default {
esbuild({
target: 'node22', // 目标为 Node.js 14
minify: false, // 启用代码压缩
tsconfig: 'tsconfig.json'
tsconfig: 'tsconfig.json',
}),
json(),
// typescript({
@ -70,3 +73,5 @@ export default {
// 将 sqlite3 作为外部依赖
external: ['sqlite3', 'sequelize', 'vite', 'sequelize', '@kevisual/router', 'ioredis', 'socket.io', 'minio'],
};
export default [config];

View File

@ -1,3 +1,4 @@
import { App } from '@kevisual/router';
import { app } from './app.ts';
import './route/system-config/index.ts';
@ -15,3 +16,7 @@ export const runApp = async (msg: Message) => {
}
return res;
};
export const loadApp = async (mainApp: App) => {
mainApp.importApp(app);
};

View File

@ -2,30 +2,11 @@ import { App } from '@kevisual/router';
import fs from 'fs';
import { getConfig, getPidList, pidFilePath, checkFileExists } from './module/get-config.ts';
import { sequelize } from './module/sequelize.ts';
import { spawn } from 'child_process';
export { sequelize };
export const app = new App();
app
.route({
path: 'ping',
})
.define(async (ctx) => {
ctx.body = 'pong';
})
.addTo(app);
app
.route({
path: 'demo',
key: '01',
})
.define(async (ctx) => {
ctx.body = 'Hello, World!';
})
.addTo(app);
// 处理进程退出或中断信号,确保删除 pid 文件
const cleanUp = () => {
const pidList = getPidList();
@ -63,5 +44,5 @@ export const createApp = async () => {
app.listen(21015, () => {
console.log('Server is running on port 21015', 'http://localhost:21015/api/router', 'server id', process.pid);
});
import('./route.ts');
// import('./route.ts');
};

View File

@ -121,3 +121,11 @@ const uploadFiles = async (files: string[], directory: string, { key, version }:
});
};
app.addCommand(command);
const local = new Command('local')
.description('本地部署')
.option('-k, --key <key>', 'key')
.action(() => {
console.log('local deploy');
});
app.addCommand(local);

216
src/command/init.ts Normal file
View File

@ -0,0 +1,216 @@
import { program, Command } from '@/program.ts';
import { checkFileExists, envisionPath, getConfig, writeConfig } from '@/module/index.ts';
import path from 'path';
import fs from 'fs';
import { chalk } from '@/module/chalk.ts';
import inquirer from 'inquirer';
import { spawn, spawnSync } from 'child_process';
const command = new Command('init').description('初始化应用').action((optison) => {
console.log('init');
}); //
program.addCommand(command);
const setMainAppConfig = async (mainAppPath: string) => {
const config = getConfig();
config.mainAppPath = mainAppPath;
writeConfig(config);
};
const initMain = async () => {
const mainAppPath = path.join(envisionPath, 'main-app');
const pkgPath = path.join(mainAppPath, 'package.json');
if (!checkFileExists(pkgPath)) {
fs.mkdirSync(mainAppPath, { recursive: true });
const pkg = {
name: 'main-app',
version: '1.0.0',
type: 'module',
};
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
console.log(chalk.green('初始化 main-app 成功'));
}
// 在对应的路径,安装依赖
const needDeps = ['@kevisual/router', '@kevisual/local-app-manager', '@kevisual/use-config'];
let hasPnpm = false;
try {
spawnSync('pnpm', ['--version']);
hasPnpm = true;
} catch (e) {
hasPnpm = false;
}
for (let dep of needDeps) {
console.log(chalk.green('安装依赖', dep));
const npm = hasPnpm ? 'pnpm' : 'npm';
spawnSync(npm, ['install', dep], { cwd: mainAppPath, stdio: 'inherit' });
}
// 创建 dist/app.mjs
const code = `import { App } from '@kevisual/router';
import { app as LocalApp } from '@kevisual/local-app-manager';
import { useConfig } from '@kevisual/use-config';
const config = useConfig();
const app = new App();
app.importApp(LocalApp);
const host = config.host || '127.0.0.1';
app.listen(config.port, host, () => {
console.log(\`app is running on http://\${host}:\${config.port}\`);
});
`;
const codeDistPath = path.join(mainAppPath, 'dist');
if (!checkFileExists(codeDistPath)) {
fs.mkdirSync(codeDistPath, { recursive: true });
}
fs.writeFileSync(path.join(codeDistPath, 'app.mjs'), code);
console.log(chalk.green('创建 app.mjs 成功'));
// 创建 app.config.json5
const answers = await inquirer.prompt([
{
type: 'input',
name: 'port',
message: '请输入端口号',
default: '11015',
},
{
type: 'input',
name: 'host',
message: '请输入host',
default: '127.0.0.1',
},
{
type: 'input',
name: 'appsPath',
message: '请输入apps路径',
default: path.join(mainAppPath, 'apps'),
},
]);
const json5 = {
port: Number(answers.port),
host: answers.host,
appsPath: answers.appsPath,
};
fs.writeFileSync(path.join(mainAppPath, 'app.config.json5'), JSON.stringify(json5, null, 2));
//
console.log(chalk.green('创建 app.config.json5 成功'));
setMainAppConfig(mainAppPath);
};
const checkPid = (pid: number) => {
try {
process.kill(pid, 0);
return true;
} catch (err) {
if (err.code === 'ESRCH') {
// 进程不存在
console.log(`进程 ${pid} 不存在`);
return false;
} else if (err.code === 'EPERM') {
// 没有权限访问该进程
console.log(`没有权限检查进程 ${pid}`);
return true;
} else {
// 其他错误
console.error(`检查进程 ${pid} 时出错:`, err);
return false;
}
}
};
const mainApp = new Command('main')
.description('初始化main的应用') //
.option('-i, --init', '初始化main应用')
.option('-s, --start', '启动main应用')
.option('-r, --restart', '重启main应用')
.option('-e, --exit', '停止main应用')
.action(async (options) => {
if (options.init) {
await initMain();
return;
}
const runChild = () => {
const logDir = path.join(envisionPath, 'log');
const logFile = path.join(logDir, 'child.log');
// 确保日志目录存在
fs.mkdirSync(logDir, { recursive: true });
// 如果日志文件不存在,创建一个
if (!checkFileExists(logFile)) {
fs.writeFileSync(logFile, '');
}
// 如果日志文件大于 10M重命名
const stats = fs.statSync(logFile);
if (stats.size > 1024 * 1024 * 10) {
fs.renameSync(logFile, path.join(logDir, `child-${Date.now()}.log`));
fs.writeFileSync(logFile, '');
}
// 打开日志文件(追加模式)
const logStream = fs.openSync(logFile, 'a');
const childProcess = spawn('node', ['dist/app.mjs'], {
cwd: getConfig().mainAppPath,
stdio: ['ignore', logStream, logStream], // 忽略 stdio, 重定向到文件
detached: true, // 使子进程独立运行
});
childProcess.unref(); // 使子进程独立运行
writeConfig({ ...getConfig(), mainAppPid: childProcess.pid });
};
const config = getConfig();
if (!config.mainAppPath) {
console.log('请先初始化 main 应用');
return;
}
if (options.start) {
if (config.mainAppPid) {
// 检查是否有进程
if (checkPid(config.mainAppPid)) {
console.log('main app 已经启动');
return;
}
}
runChild();
}
if (options.restart) {
if (config.mainAppPid) {
// 检查是否有进程
if (checkPid(config.mainAppPid)) {
process.kill(config.mainAppPid);
}
}
runChild();
} else if (options.stop) {
if (config.mainAppPid) {
// 检查是否有进程
if (checkPid(config.mainAppPid)) {
process.kill(config.mainAppPid);
}
writeConfig({ ...config, mainAppPid: null });
}
}
});
const mainLogCommand = new Command('log')
.option('-t, --tail', '查看日志')
.option('-f, --follow', '跟踪日志')
.description('查看main的日志')
.action((options) => {
const logDir = path.join(envisionPath, 'log');
const logFile = path.join(logDir, 'child.log');
if (!checkFileExists(logFile)) {
console.log('日志文件不存在');
return;
}
if (options.tail) {
const childProcess = spawn('tail', ['-n', '20', '-f', logFile]);
childProcess.stdout?.pipe(process.stdout);
childProcess.stderr?.pipe(process.stderr);
return;
}
if (options.follow) {
const childProcess = spawn('tail', ['-n', '50', '-f', logFile]);
childProcess.stdout?.pipe(process.stdout);
childProcess.stderr?.pipe(process.stderr);
return;
}
const log = fs.readFileSync(logFile, 'utf-8');
console.log(log);
});
mainApp.addCommand(mainLogCommand);
program.addCommand(mainApp);

View File

@ -7,69 +7,12 @@ import { getConfig } from '@/module/get-config.ts';
import fs from 'fs';
import inquirer from 'inquirer';
const command = new Command('npm')
.description('npm command show publish and set .npmrc')
.option('-p --publish <publish>', 'publish')
.action(async (options) => {
const { publish } = options || {};
const config = getConfig();
let cmd = '';
const execPath = process.cwd();
if (publish) {
const packageJson = path.resolve(execPath, 'package.json');
switch (publish) {
case 'me':
cmd = 'npm publish --registry https://npm.xiongxiao.me';
console.log(chalk.green(cmd));
break;
case 'npm':
cmd = 'npm publish --registry https://registry.npmjs.org';
console.log(chalk.green(cmd));
break;
default:
cmd = 'npm publish --registry https://npm.xiongxiao.me';
console.log(chalk.green(cmd));
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 child = spawn(cmd, {
shell: true,
cwd: execPath,
env: {
...process.env, // 保留当前环境变量
...tokenEnv,
},
});
child.stdout.on('data', (data) => {
console.log(chalk.green(`${data}`));
});
child.stderr.on('data', (data) => {
// 过滤掉 'npm notice' 或者其他信息
if (data.toString().includes('npm notice')) {
console.log(chalk.yellow(`notice: ${data}`));
} else {
console.error(`stderr: ${data}`);
}
});
child.on('close', (code) => {
// console.log(`child process exited with code ${code}`);
});
} else {
console.error(chalk.red('package.json not found'));
}
}
});
const command = new Command('npm').description('npm command show publish and set .npmrc').action(async (options) => {});
const publish = new Command('publish')
.argument('[registry]')
.option('-p --proxy', 'proxy')
.description('publish npm')
.action(async (registry) => {
.action(async (registry, options) => {
const answer = await inquirer.prompt([
{
type: 'list',
@ -92,6 +35,18 @@ const publish = new Command('publish')
const config = getConfig();
let cmd = '';
const execPath = process.cwd();
let setEnv = {};
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 (options?.proxy) {
setEnv = {
...proxyEnv,
};
}
if (registry) {
const packageJson = path.resolve(execPath, 'package.json');
switch (registry) {
@ -122,6 +77,7 @@ const publish = new Command('publish')
env: {
...process.env, // 保留当前环境变量
...tokenEnv,
...setEnv,
},
});
child.stdout.on('data', (data) => {
@ -187,7 +143,6 @@ const npmrc = new Command('set')
writeFlag = true;
}
}
return;
} else {
writeFlag = true;
}

30
src/command/proxy.ts Normal file
View File

@ -0,0 +1,30 @@
import { program, Command } from '@/program.ts';
import { chalk } from '@/module/chalk.ts';
import inquirer from 'inquirer';
const command = new Command('proxy')
.description('执行代理相关的命令')
.option('-s, --start', '启动代理')
.option('-u, --unset', '关闭代理')
.action((options) => {
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 (options.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 (options.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'));
}
});
program.addCommand(command);

View File

@ -1,7 +1,4 @@
import { program as app, Command } from '@/program.ts';
import { getConfig, writeConfig, checkFileExists } from '@/module/index.ts';
import { createApp } from '@/app.ts';
import fs from 'fs';
import inquirer from 'inquirer';
import { query } from '../module/index.ts';
import chalk from 'chalk';

View File

@ -10,10 +10,13 @@ import './command/web.ts';
import './command/router.ts';
import './command/npm.ts';
import './command/publish.ts';
import './command/init.ts';
import './command/proxy.ts';
// program.parse(process.argv);
export const runParser = async (argv: string[]) => {
// program.parse(process.argv);
// console.log('argv', argv);
program.parse(argv);
};

View File

@ -1,8 +1,13 @@
import { program, Command } from 'commander';
import fs from 'fs';
// 将多个子命令加入主程序中
program.name('app').description('A CLI tool with envison').version('0.0.3');
let version = '0.0.1';
try {
// @ts-ignore
if (VERSION) version = VERSION;
} catch (e) {}
// @ts-ignore
program.name('app').description('A CLI tool with envison').version(version);
const ls = new Command('ls').description('List files in the current directory').action(() => {
console.log('List files');

View File

@ -1,3 +1 @@
import './route/user/list.ts';
import './route/system-config/index.ts';

View File

@ -1,6 +1,5 @@
import { app } from '@/app.ts';
import { Config } from './model/config.ts';
import { CustomError } from '@kevisual/router';
app
.route({
path: 'config',
@ -35,7 +34,7 @@ app
.define(async (ctx) => {
const { data } = ctx.query;
if (!data) {
throw new CustomError('data is required');
ctx.throw(400, 'data is required');
}
let config = await Config.findOne({
where: { key: 'me' }, // 自定义条件

View File

@ -1,11 +0,0 @@
import { app } from '@/app.ts';
app
.route({
path: 'user',
key: 'list',
})
.define(async (ctx) => {
ctx.body = 'user list';
})
.addTo(app);