merge routes-packages
This commit is contained in:
7
assistant/src/module/local-apps/.gitignore
vendored
Normal file
7
assistant/src/module/local-apps/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
node_modules
|
||||||
|
/apps
|
||||||
|
dist
|
||||||
|
/logs
|
||||||
|
|
||||||
|
/app.config.json5
|
||||||
|
/app.config.json
|
||||||
3
assistant/src/module/local-apps/.npmrc
Normal file
3
assistant/src/module/local-apps/.npmrc
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN}
|
||||||
|
@abearxiong:registry=https://npm.pkg.github.com
|
||||||
|
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
|
||||||
87
assistant/src/module/local-apps/apps.config.json
Normal file
87
assistant/src/module/local-apps/apps.config.json
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
{
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"key": "demo",
|
||||||
|
"status": "inactive",
|
||||||
|
"type": "system-app",
|
||||||
|
"description": "",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"runtime": [],
|
||||||
|
"entry": "demo-dist/app.mjs",
|
||||||
|
"path": "/home/ubuntu/kevisual/cli/assistant/src/module/local-apps/apps/demo",
|
||||||
|
"origin": {
|
||||||
|
"key": "demo",
|
||||||
|
"entry": "demo-dist/app.mjs",
|
||||||
|
"type": "system-app"
|
||||||
|
},
|
||||||
|
"timestamp": 1764867997389
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "test-ts",
|
||||||
|
"status": "inactive",
|
||||||
|
"type": "system-app",
|
||||||
|
"description": "",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"runtime": [],
|
||||||
|
"entry": "main.ts",
|
||||||
|
"path": "/home/ubuntu/kevisual/cli/assistant/src/module/local-apps/apps/test-ts",
|
||||||
|
"origin": {
|
||||||
|
"key": "test-ts",
|
||||||
|
"entry": "main.ts",
|
||||||
|
"type": "system-app"
|
||||||
|
},
|
||||||
|
"timestamp": 1764867997389
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "test",
|
||||||
|
"status": "inactive",
|
||||||
|
"type": "pm2-system-app",
|
||||||
|
"description": "",
|
||||||
|
"version": "0.0.3",
|
||||||
|
"runtime": [],
|
||||||
|
"entry": "dist/ws.js",
|
||||||
|
"path": "/home/ubuntu/kevisual/cli/assistant/src/module/local-apps/apps/test",
|
||||||
|
"origin": {
|
||||||
|
"key": "test",
|
||||||
|
"entry": "dist/ws.js",
|
||||||
|
"type": "pm2-system-app"
|
||||||
|
},
|
||||||
|
"pm2Options": {
|
||||||
|
"cwd": "/home/ubuntu/kevisual/cli/assistant/src/module/local-apps"
|
||||||
|
},
|
||||||
|
"timestamp": 1764867997389
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "root/test-log",
|
||||||
|
"status": "inactive",
|
||||||
|
"type": "script-app",
|
||||||
|
"description": "",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"runtime": [],
|
||||||
|
"entry": "main.ts",
|
||||||
|
"path": "/home/ubuntu/kevisual/cli/assistant/src/module/local-apps/apps/root/test-log",
|
||||||
|
"origin": {
|
||||||
|
"key": "root/test-log",
|
||||||
|
"entry": "main.ts",
|
||||||
|
"type": "script-app"
|
||||||
|
},
|
||||||
|
"timestamp": 1764867997389
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "root/test-no",
|
||||||
|
"status": "inactive",
|
||||||
|
"type": "script-app",
|
||||||
|
"description": "",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"runtime": [],
|
||||||
|
"entry": "main.ts",
|
||||||
|
"path": "/home/ubuntu/kevisual/cli/assistant/src/module/local-apps/apps/root/test-no",
|
||||||
|
"origin": {
|
||||||
|
"entry": "main.ts",
|
||||||
|
"type": "script-app",
|
||||||
|
"key": "root/test-no"
|
||||||
|
},
|
||||||
|
"timestamp": 1764868133979
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
45
assistant/src/module/local-apps/bun.config.ts
Normal file
45
assistant/src/module/local-apps/bun.config.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { build } from 'bun';
|
||||||
|
|
||||||
|
const external = ['pm2', '@kevisual/router', '@kevisual/use-config'];
|
||||||
|
// https://bun.sh/docs/bundler
|
||||||
|
await Bun.build({
|
||||||
|
target: 'node',
|
||||||
|
format: 'esm',
|
||||||
|
entrypoints: ['./src/app.ts'],
|
||||||
|
outdir: './dist',
|
||||||
|
naming: {
|
||||||
|
entry: 'app.js',
|
||||||
|
},
|
||||||
|
external,
|
||||||
|
minify: false,
|
||||||
|
sourcemap: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const dts = 'dts -i src/app.ts -o app.d.ts';
|
||||||
|
await Bun.spawn({
|
||||||
|
cmd: dts.split(' '),
|
||||||
|
stdout: 'inherit',
|
||||||
|
stderr: 'inherit',
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
await Bun.build({
|
||||||
|
target: 'node',
|
||||||
|
format: 'esm',
|
||||||
|
entrypoints: ['./src/manager.ts'],
|
||||||
|
outdir: './dist',
|
||||||
|
naming: {
|
||||||
|
entry: 'manager.js',
|
||||||
|
},
|
||||||
|
external,
|
||||||
|
minify: false,
|
||||||
|
sourcemap: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const dtsManager = 'dts -i src/manager.ts -o manager.d.ts';
|
||||||
|
await Bun.spawn({
|
||||||
|
cmd: dtsManager.split(' '),
|
||||||
|
stdout: 'inherit',
|
||||||
|
stderr: 'inherit',
|
||||||
|
});
|
||||||
|
console.log('Build completed.');
|
||||||
18
assistant/src/module/local-apps/demos/demo/demo-dist/app.mjs
Normal file
18
assistant/src/module/local-apps/demos/demo/demo-dist/app.mjs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { App } from '@kevisual/router';
|
||||||
|
const app = new App();
|
||||||
|
|
||||||
|
app
|
||||||
|
.route({
|
||||||
|
path: 'demo',
|
||||||
|
key: 'get'
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
ctx.body = 'Hello, World!';
|
||||||
|
})
|
||||||
|
.addTo(app);
|
||||||
|
|
||||||
|
const loadApp = (mainApp, appInfo) => {
|
||||||
|
mainApp.importApp(app);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { app, loadApp };
|
||||||
19
assistant/src/module/local-apps/demos/demo/package.json
Normal file
19
assistant/src/module/local-apps/demos/demo/package.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "demo",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "",
|
||||||
|
"main": "demo-dist/app.mjs",
|
||||||
|
"app": {
|
||||||
|
"entry": "demo-dist/app.mjs",
|
||||||
|
"type": "system-app"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"ai"
|
||||||
|
],
|
||||||
|
"author": "abearxiong <xiongxiao@xiongxiao.me>",
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { Logger } from '@kevisual/logger';
|
||||||
|
|
||||||
|
const logger = new Logger();
|
||||||
|
|
||||||
|
logger.info('This is a test log message from the main.ts file of the test app.');
|
||||||
|
logger.warn('This is a test warning message from the main.ts file of the test app.');
|
||||||
|
logger.error('This is a test error message from the main.ts file of the test app.');
|
||||||
|
logger.debug('This is a test debug message from the main.ts file of the test app.');
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "test-ts",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "",
|
||||||
|
"basename": "/root/test-log",
|
||||||
|
"app": {
|
||||||
|
"entry": "main.ts",
|
||||||
|
"type": "pm2-system-app"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"main.ts"
|
||||||
|
],
|
||||||
|
"keywords": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { Logger } from '@kevisual/logger';
|
||||||
|
|
||||||
|
const logger = new Logger();
|
||||||
|
|
||||||
|
logger.info('This is a test log message from the main.ts file of the test app.');
|
||||||
|
logger.warn('This is a test warning message from the main.ts file of the test app.');
|
||||||
|
logger.error('This is a test error message from the main.ts file of the test app.');
|
||||||
|
logger.debug('This is a test debug message from the main.ts file of the test app.');
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "test-no",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"basename": "/root/test-no",
|
||||||
|
"app": {
|
||||||
|
"entry": "main.ts",
|
||||||
|
"type": "script-app"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
48
assistant/src/module/local-apps/package.json
Normal file
48
assistant/src/module/local-apps/package.json
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"name": "@kevisual/local-app-manager",
|
||||||
|
"version": "0.1.32",
|
||||||
|
"description": "",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"entry": "app.js",
|
||||||
|
"type": "system-app"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "bun run src/app.ts",
|
||||||
|
"build": "rimraf dist && bun run bun.config.ts"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "abearxiong <xiongxiao@xiongxiao.me>",
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "module",
|
||||||
|
"devDependencies": {
|
||||||
|
"@kevisual/router": "0.0.33",
|
||||||
|
"@kevisual/types": "^0.0.10",
|
||||||
|
"@kevisual/use-config": "^1.0.21",
|
||||||
|
"@types/node": "^24.10.1",
|
||||||
|
"fast-glob": "^3.3.3",
|
||||||
|
"lodash-es": "^4.17.21"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/app.mjs",
|
||||||
|
"types": "./dist/app.d.ts",
|
||||||
|
"require": "./dist/app.mjs"
|
||||||
|
},
|
||||||
|
"./manager": {
|
||||||
|
"import": "./dist/manager.mjs",
|
||||||
|
"types": "./dist/manager.d.ts",
|
||||||
|
"require": "./dist/manager.mjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"pm2": "^6.0.14"
|
||||||
|
}
|
||||||
|
}
|
||||||
191
assistant/src/module/local-apps/readme.md
Normal file
191
assistant/src/module/local-apps/readme.md
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
# 本地应用加载
|
||||||
|
|
||||||
|
主要目的,加载微应用模块。
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export enum AppType {
|
||||||
|
/**
|
||||||
|
* run in (import way)
|
||||||
|
*/
|
||||||
|
SystemApp = 'system-app',
|
||||||
|
/**
|
||||||
|
* fork 执行
|
||||||
|
*/
|
||||||
|
MicroApp = 'micro-app',
|
||||||
|
GatewayApp = 'gateway-app',
|
||||||
|
/**
|
||||||
|
* pm2 启动
|
||||||
|
*/
|
||||||
|
Pm2SystemApp = 'pm2-system-app'
|
||||||
|
}
|
||||||
|
export type Runtime = 'client' | 'server';
|
||||||
|
export type AppInfo = {
|
||||||
|
key: string;
|
||||||
|
status?: 'inactive' | 'running' | 'stop' | 'error'; // 运行状态
|
||||||
|
version?: string; // 版本
|
||||||
|
type?: AppType; // 默认类型
|
||||||
|
description?: string; // 描述
|
||||||
|
runtime?: Runtime[]; // 运行时
|
||||||
|
timestamp?: number; // 时间戳, 每次更新更新时间戳
|
||||||
|
process?: any; // 进程
|
||||||
|
|
||||||
|
origin?: Record<string, any>; // 原始数据
|
||||||
|
entry?: string; // 入口文件
|
||||||
|
path?: string; // 文件路径
|
||||||
|
env?: Record<string, any>; // 环境变量
|
||||||
|
engine?: string; // runtime, python node deno bun etc
|
||||||
|
/**
|
||||||
|
* pm2 选项, 仅仅当是AppType.Pm2SystemApp的时候生效
|
||||||
|
* pm2 选项可以参考 https://pm2.keymetrics.io/docs/usage/application-declaration/
|
||||||
|
*/
|
||||||
|
pm2Options?: StartOptions; // pm2 选项
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
pm2 pm2Options
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface StartOptions {
|
||||||
|
/**
|
||||||
|
* Enable or disable auto start after process added (default: true).
|
||||||
|
*/
|
||||||
|
autostart?: boolean;
|
||||||
|
/**
|
||||||
|
* Enable or disable auto restart after process failure (default: true).
|
||||||
|
*/
|
||||||
|
autorestart?: boolean;
|
||||||
|
/**
|
||||||
|
* List of exit codes that should allow the process to stop (skip autorestart).
|
||||||
|
*/
|
||||||
|
stop_exit_codes?: number[];
|
||||||
|
/**
|
||||||
|
* An arbitrary name that can be used to interact with (e.g. restart) the process
|
||||||
|
* later in other commands. Defaults to the script name without its extension
|
||||||
|
* (eg “testScript” for “testScript.js”)
|
||||||
|
*/
|
||||||
|
name?: string;
|
||||||
|
/**
|
||||||
|
* The path of the script to run
|
||||||
|
*/
|
||||||
|
script?: string;
|
||||||
|
/**
|
||||||
|
* A string or array of strings composed of arguments to pass to the script.
|
||||||
|
*/
|
||||||
|
args?: string | string[];
|
||||||
|
/**
|
||||||
|
* A string or array of strings composed of arguments to call the interpreter process with.
|
||||||
|
* Eg “–harmony” or [”–harmony”,”–debug”]. Only applies if interpreter is something other
|
||||||
|
* than “none” (its “node” by default).
|
||||||
|
*/
|
||||||
|
interpreter_args?: string | string[];
|
||||||
|
/**
|
||||||
|
* The working directory to start the process with.
|
||||||
|
*/
|
||||||
|
cwd?: string;
|
||||||
|
/**
|
||||||
|
* (Default: “~/.pm2/logs/app_name-out.log”) The path to a file to append stdout output to.
|
||||||
|
* Can be the same file as error.
|
||||||
|
*/
|
||||||
|
output?: string;
|
||||||
|
/**
|
||||||
|
* (Default: “~/.pm2/logs/app_name-error.err”) The path to a file to append stderr output to. Can be the same file as output.
|
||||||
|
*/
|
||||||
|
error?: string;
|
||||||
|
/**
|
||||||
|
* The display format for log timestamps (eg “YYYY-MM-DD HH:mm Z”). The format is a moment display format.
|
||||||
|
*/
|
||||||
|
log_date_format?: string;
|
||||||
|
/**
|
||||||
|
* Default: “~/.pm2/logs/~/.pm2/pids/app_name-id.pid”)
|
||||||
|
* The path to a file to write the pid of the started process. The file will be overwritten.
|
||||||
|
* Note that the file is not used in any way by pm2 and so the user is free to manipulate or
|
||||||
|
* remove that file at any time. The file will be deleted when the process is stopped or the daemon killed.
|
||||||
|
*/
|
||||||
|
pid?: string;
|
||||||
|
/**
|
||||||
|
* The minimum uptime of the script before it’s considered successfully started.
|
||||||
|
*/
|
||||||
|
min_uptime?: number;
|
||||||
|
/**
|
||||||
|
* The maximum number of times in a row a script will be restarted if it exits in less than min_uptime.
|
||||||
|
*/
|
||||||
|
max_restarts?: number;
|
||||||
|
/**
|
||||||
|
* If sets and script’s memory usage goes about the configured number, pm2 restarts the script.
|
||||||
|
* Uses human-friendly suffixes: ‘K’ for kilobytes, ‘M’ for megabytes, ‘G’ for gigabytes’, etc. Eg “150M”.
|
||||||
|
*/
|
||||||
|
max_memory_restart?: number | string;
|
||||||
|
/**
|
||||||
|
* Arguments to pass to the interpreter
|
||||||
|
*/
|
||||||
|
node_args?: string | string[];
|
||||||
|
/**
|
||||||
|
* Prefix logs with time
|
||||||
|
*/
|
||||||
|
time?: boolean;
|
||||||
|
/**
|
||||||
|
* This will make PM2 listen for that event. In your application you will need to add process.send('ready');
|
||||||
|
* when you want your application to be considered as ready.
|
||||||
|
*/
|
||||||
|
wait_ready?: boolean;
|
||||||
|
/**
|
||||||
|
* (Default: 1600)
|
||||||
|
* The number of milliseconds to wait after a stop or restart command issues a SIGINT signal to kill the
|
||||||
|
* script forceably with a SIGKILL signal.
|
||||||
|
*/
|
||||||
|
kill_timeout?: number;
|
||||||
|
/**
|
||||||
|
* (Default: 0) Number of millseconds to wait before restarting a script that has exited.
|
||||||
|
*/
|
||||||
|
restart_delay?: number;
|
||||||
|
/**
|
||||||
|
* (Default: “node”) The interpreter for your script (eg “python”, “ruby”, “bash”, etc).
|
||||||
|
* The value “none” will execute the ‘script’ as a binary executable.
|
||||||
|
*/
|
||||||
|
interpreter?: string;
|
||||||
|
/**
|
||||||
|
* (Default: ‘fork’) If sets to ‘cluster’, will enable clustering
|
||||||
|
* (running multiple instances of the script).
|
||||||
|
*/
|
||||||
|
exec_mode?: string;
|
||||||
|
/**
|
||||||
|
* (Default: 1) How many instances of script to create. Only relevant in exec_mode ‘cluster’.
|
||||||
|
*/
|
||||||
|
instances?: number;
|
||||||
|
/**
|
||||||
|
* (Default: false) If true, merges the log files for all instances of script into one stderr log
|
||||||
|
* and one stdout log. Only applies in ‘cluster’ mode. For example, if you have 4 instances of
|
||||||
|
* ‘test.js’ started via pm2, normally you would have 4 stdout log files and 4 stderr log files,
|
||||||
|
* but with this option set to true you would only have one stdout file and one stderr file.
|
||||||
|
*/
|
||||||
|
merge_logs?: boolean;
|
||||||
|
/**
|
||||||
|
* If set to true, the application will be restarted on change of the script file.
|
||||||
|
*/
|
||||||
|
watch?: boolean | string[];
|
||||||
|
/**
|
||||||
|
* (Default: false) By default, pm2 will only start a script if that script isn’t
|
||||||
|
* already running (a script is a path to an application, not the name of an application
|
||||||
|
* already running). If force is set to true, pm2 will start a new instance of that script.
|
||||||
|
*/
|
||||||
|
force?: boolean;
|
||||||
|
ignore_watch?: string[];
|
||||||
|
cron?: any;
|
||||||
|
execute_command?: any;
|
||||||
|
write?: any;
|
||||||
|
source_map_support?: any;
|
||||||
|
disable_source_map_support?: any;
|
||||||
|
/**
|
||||||
|
* The environment variables to pass on to the process.
|
||||||
|
*/
|
||||||
|
env?: { [key: string]: string };
|
||||||
|
/**
|
||||||
|
* NameSpace for the process
|
||||||
|
* @default 'default'
|
||||||
|
* @example 'production'
|
||||||
|
* @example 'development'
|
||||||
|
* @example 'staging'
|
||||||
|
*/
|
||||||
|
namespace?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
3
assistant/src/module/local-apps/src/app.ts
Normal file
3
assistant/src/module/local-apps/src/app.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { App } from '@kevisual/router';
|
||||||
|
import { useContextKey } from '@kevisual/use-config/context';
|
||||||
|
export const app = useContextKey('app', () => new App());
|
||||||
43
assistant/src/module/local-apps/src/index.ts
Normal file
43
assistant/src/module/local-apps/src/index.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { App } from '@kevisual/router';
|
||||||
|
import { app } from './app.ts';
|
||||||
|
|
||||||
|
import { manager, loadManager } from './manager.ts';
|
||||||
|
|
||||||
|
import './routes/list.ts';
|
||||||
|
|
||||||
|
export { app, manager, loadManager };
|
||||||
|
|
||||||
|
if (DEV_SERVER) {
|
||||||
|
app
|
||||||
|
.route({
|
||||||
|
path: 'auth',
|
||||||
|
key: 'admin',
|
||||||
|
id: 'auth-admin'
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
// ctx.body = 'admin';
|
||||||
|
})
|
||||||
|
.addTo(app);
|
||||||
|
app
|
||||||
|
.route({
|
||||||
|
path: 'test',
|
||||||
|
key: 'test'
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
ctx.body = app.router.routes.map((item) => {
|
||||||
|
return {
|
||||||
|
path: item.path,
|
||||||
|
key: item.key
|
||||||
|
};
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.addTo(app);
|
||||||
|
app.listen(9787, () => {
|
||||||
|
console.log('Server is running on port 9787');
|
||||||
|
});
|
||||||
|
loadManager({ configFilename: 'b.json' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadApp = (mainApp: App, appInfo?: any) => {
|
||||||
|
//
|
||||||
|
};
|
||||||
23
assistant/src/module/local-apps/src/manager.ts
Normal file
23
assistant/src/module/local-apps/src/manager.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { app } from './app.ts';
|
||||||
|
import { LoadOptions, Manager } from './modules/manager.ts';
|
||||||
|
|
||||||
|
export const manager = new Manager({
|
||||||
|
mainApp: app
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载manager的内容
|
||||||
|
*/
|
||||||
|
export const loadManager = (opts?: LoadOptions) => {
|
||||||
|
const load = () => {
|
||||||
|
manager
|
||||||
|
.load(opts)
|
||||||
|
.then(() => {
|
||||||
|
console.log('load apps success');
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('load apps error', err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
setTimeout(load, 1000);
|
||||||
|
};
|
||||||
82
assistant/src/module/local-apps/src/modules/app-file.ts
Normal file
82
assistant/src/module/local-apps/src/modules/app-file.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { useFileStore } from '@kevisual/use-config/file-store';
|
||||||
|
import { fileIsExist, getConfigFile } from '@kevisual/use-config/env';
|
||||||
|
import path from 'node:path';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import { AppInfo } from './manager.ts';
|
||||||
|
|
||||||
|
export const getAppsPath = () => {
|
||||||
|
const appsPath = process.env.APPS_PATH;
|
||||||
|
if (appsPath) {
|
||||||
|
const resolvePath = path.resolve(appsPath);
|
||||||
|
if (fileIsExist(resolvePath)) {
|
||||||
|
return resolvePath;
|
||||||
|
} else {
|
||||||
|
fs.mkdirSync(resolvePath, { recursive: true });
|
||||||
|
return resolvePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return useFileStore('apps', { needExists: true });
|
||||||
|
};
|
||||||
|
export type AppInfoConfig = {
|
||||||
|
list: AppInfo[];
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 加载应用信息
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const loadAppInfo = async (appsPath: string = 'apps', filename = 'apps.config.json'): Promise<AppInfoConfig> => {
|
||||||
|
let configFile = getConfigFile({
|
||||||
|
cwd: appsPath,
|
||||||
|
fileName: filename
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!configFile) {
|
||||||
|
configFile = path.join(appsPath, '..', filename);
|
||||||
|
fs.writeFileSync(configFile, JSON.stringify({ list: [] }));
|
||||||
|
return { list: [] };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const config = fs.readFileSync(configFile, 'utf-8');
|
||||||
|
const v = JSON.parse(config);
|
||||||
|
if (!v.list) {
|
||||||
|
v.list = [];
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('读取配置文件失败', e.message);
|
||||||
|
return { list: [] };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* 保存应用信息
|
||||||
|
* @param data
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const saveAppInfo = async (data: any, appsPath: string, filename = 'apps.config.json') => {
|
||||||
|
const configFile = getConfigFile({
|
||||||
|
fileName: filename,
|
||||||
|
cwd: appsPath
|
||||||
|
});
|
||||||
|
if (!configFile) {
|
||||||
|
console.error('未找到配置文件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fs.writeFileSync(configFile, JSON.stringify(data, null, 2));
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 删除应用信息
|
||||||
|
* @param key
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const deleteFileAppInfo = async (key: string, appsPath: string) => {
|
||||||
|
// 标准化key中的路径分隔符,统一使用系统路径分隔符
|
||||||
|
const normalizedKey = key.replace(/\//g, path.sep);
|
||||||
|
const directory = path.join(appsPath, normalizedKey);
|
||||||
|
if (!fileIsExist(directory)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fs.rmSync(directory, { recursive: true });
|
||||||
|
};
|
||||||
576
assistant/src/module/local-apps/src/modules/manager.ts
Normal file
576
assistant/src/module/local-apps/src/modules/manager.ts
Normal file
@@ -0,0 +1,576 @@
|
|||||||
|
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 { deleteFileAppInfo } from './app-file.ts';
|
||||||
|
import { fileIsExist } from '@kevisual/use-config/env';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import glob from 'fast-glob'
|
||||||
|
import { Pm2Manager, Pm2Connect, checkInstall } from './pm2.ts';
|
||||||
|
import type { StartOptions } from 'pm2';
|
||||||
|
export { Pm2Manager, Pm2Connect };
|
||||||
|
// 共享
|
||||||
|
export const existDenpend = [
|
||||||
|
'sequelize', // commonjs
|
||||||
|
'pg', // commonjs
|
||||||
|
'@kevisual/router', // 共享模块
|
||||||
|
'@kevisual/use-config', // 共享模块
|
||||||
|
'ioredis', // commonjs
|
||||||
|
'socket.io', // commonjs
|
||||||
|
'minio', // commonjs
|
||||||
|
];
|
||||||
|
export enum AppType {
|
||||||
|
/**
|
||||||
|
* run in (import way)
|
||||||
|
*/
|
||||||
|
SystemApp = 'system-app',
|
||||||
|
/**
|
||||||
|
* fork 执行
|
||||||
|
*/
|
||||||
|
MicroApp = 'micro-app',
|
||||||
|
GatewayApp = 'gateway-app',
|
||||||
|
/**
|
||||||
|
* pm2 启动
|
||||||
|
*/
|
||||||
|
Pm2SystemApp = 'pm2-system-app',
|
||||||
|
ScriptApp = 'script-app'
|
||||||
|
}
|
||||||
|
export type Runtime = 'client' | 'server';
|
||||||
|
export type AppInfo = {
|
||||||
|
key: string;
|
||||||
|
status?: 'inactive' | 'running' | 'stop' | 'error' | 'finished'; // 运行状态
|
||||||
|
version?: string; // 版本
|
||||||
|
type?: AppType; // 默认类型
|
||||||
|
description?: string; // 描述
|
||||||
|
runtime?: Runtime[]; // 运行时
|
||||||
|
timestamp?: number; // 时间戳, 每次更新更新时间戳
|
||||||
|
process?: any; // 进程
|
||||||
|
|
||||||
|
origin?: Record<string, any>; // 原始数据
|
||||||
|
entry?: string; // 入口文件
|
||||||
|
path?: string; // 文件路径
|
||||||
|
env?: Record<string, any>; // 环境变量
|
||||||
|
engine?: string; // runtime, python node deno bun etc
|
||||||
|
/**
|
||||||
|
* pm2 选项, 仅仅当是AppType.Pm2SystemApp的时候生效
|
||||||
|
* pm2 选项可以参考 https://pm2.keymetrics.io/docs/usage/application-declaration/
|
||||||
|
*/
|
||||||
|
pm2Options?: StartOptions; // pm2 选项
|
||||||
|
};
|
||||||
|
export const onAppShowInfo = (app: AppInfo) => {
|
||||||
|
return {
|
||||||
|
key: app.key,
|
||||||
|
status: app.status,
|
||||||
|
engine: app.engine,
|
||||||
|
type: app.type,
|
||||||
|
description: app.description,
|
||||||
|
version: app.version
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export const createAppShowInfo = (app: any) => {
|
||||||
|
return {
|
||||||
|
key: app.key,
|
||||||
|
status: app.status,
|
||||||
|
type: app.type,
|
||||||
|
engine: app.engine,
|
||||||
|
description: app.description,
|
||||||
|
version: app.version
|
||||||
|
};
|
||||||
|
};
|
||||||
|
type managerOptions = {
|
||||||
|
mainApp?: App;
|
||||||
|
/**
|
||||||
|
* apps文件夹的路径
|
||||||
|
* @default process.env.APPS_PATH
|
||||||
|
* @default useFileStore('apps', { needExists: true })
|
||||||
|
* @example /path/to/apps
|
||||||
|
*/
|
||||||
|
appsPath?: string;
|
||||||
|
/**
|
||||||
|
* apps.config.json的路径
|
||||||
|
* @default appsPath/apps.config.json
|
||||||
|
* @example /path/to/apps/apps.config.json
|
||||||
|
*/
|
||||||
|
configFilename?: string;
|
||||||
|
};
|
||||||
|
export type LoadOptions = { runtime?: Runtime } & managerOptions;
|
||||||
|
export class Manager<T extends AppInfo = AppInfo> {
|
||||||
|
apps: Map<string, T>;
|
||||||
|
mainApp?: App;
|
||||||
|
appInfo: AppInfoConfig;
|
||||||
|
appsPath: string;
|
||||||
|
configFilename: string;
|
||||||
|
constructor(opts: managerOptions) {
|
||||||
|
this.apps = new Map();
|
||||||
|
this.mainApp = opts.mainApp;
|
||||||
|
this.appInfo = {} as any;
|
||||||
|
this.appsPath = opts?.appsPath || getAppsPath();
|
||||||
|
this.configFilename = opts?.configFilename || 'apps.config.json';
|
||||||
|
}
|
||||||
|
#pm2Connect?: Pm2Connect;
|
||||||
|
/**
|
||||||
|
* 检查key是否存在
|
||||||
|
* @param key
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
checkKey(key: string) {
|
||||||
|
return this.apps.has(key);
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* 获取app信息
|
||||||
|
* @param key
|
||||||
|
*/
|
||||||
|
async loadApp(app: T) {
|
||||||
|
const mainApp = this.mainApp;
|
||||||
|
this.apps.set(app.key, app);
|
||||||
|
if (app.status !== 'running') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!fileIsExist(app.path)) {
|
||||||
|
console.error('app is not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pathEntry = path.join(app.path, app.entry);
|
||||||
|
if (!fileIsExist(pathEntry)) {
|
||||||
|
console.error('file entry not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const entry = app.entry + `?timestamp=${app?.timestamp}`;
|
||||||
|
// 注册路由
|
||||||
|
if (app.type === AppType.MicroApp) {
|
||||||
|
const childProcess = fork(app.entry, [], {
|
||||||
|
stdio: 'inherit', // 共享主进程的标准输入输出
|
||||||
|
cwd: app.path,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
...app.env,
|
||||||
|
APP_KEY: app.key,
|
||||||
|
APP_PATH: app.path,
|
||||||
|
APP_ENTRY: entry
|
||||||
|
}
|
||||||
|
});
|
||||||
|
app.process = childProcess;
|
||||||
|
} else if (app.type === AppType.SystemApp) {
|
||||||
|
const pathEntryAndTimestamp = path.join(app.path, entry);
|
||||||
|
// Windows下需要使用file://协议,并将反斜杠转换为正斜杠
|
||||||
|
const importPath = process.platform === 'win32'
|
||||||
|
? 'file:///' + pathEntryAndTimestamp.replace(/\\/g, '/')
|
||||||
|
: pathEntryAndTimestamp;
|
||||||
|
const module = await import(importPath);
|
||||||
|
if (module.loadApp && mainApp) {
|
||||||
|
await module.loadApp?.(mainApp, app);
|
||||||
|
}
|
||||||
|
} else if (app.type === AppType.GatewayApp) {
|
||||||
|
console.log('gateway app not support');
|
||||||
|
} else if (app.type === AppType.Pm2SystemApp) {
|
||||||
|
const pathEntry = path.join(app.path, app.entry);
|
||||||
|
const pm2Manager = new Pm2Manager({
|
||||||
|
appName: app.key,
|
||||||
|
script: pathEntry,
|
||||||
|
pm2Connect: this.#pm2Connect
|
||||||
|
});
|
||||||
|
|
||||||
|
// const isInstall = await checkInstall(app);
|
||||||
|
// if (!isInstall) {
|
||||||
|
// console.log('install failed');
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
const pm2Options: StartOptions = app.pm2Options || {};
|
||||||
|
if (app?.engine) {
|
||||||
|
pm2Options.interpreter = pm2Options.interpreter || app?.engine;
|
||||||
|
}
|
||||||
|
if (!pm2Options.cwd) {
|
||||||
|
pm2Options.cwd = path.join(app.path, '../..');
|
||||||
|
}
|
||||||
|
await pm2Manager.start(pm2Options);
|
||||||
|
} else {
|
||||||
|
console.error('app type not support', app.type);
|
||||||
|
}
|
||||||
|
console.log(`load ${app.type} success`, app.key);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* create new app info
|
||||||
|
* @param app
|
||||||
|
*/
|
||||||
|
async saveAppInfo(app: T, newTimeData = false) {
|
||||||
|
const list = this.appInfo.list || [];
|
||||||
|
if (newTimeData) {
|
||||||
|
app.timestamp = Date.now();
|
||||||
|
}
|
||||||
|
const { process, ...info } = app;
|
||||||
|
const has = list.findIndex((item) => item.key === app.key);
|
||||||
|
|
||||||
|
if (has >= 0) {
|
||||||
|
list[has] = info;
|
||||||
|
} else {
|
||||||
|
list.push(info);
|
||||||
|
}
|
||||||
|
this.appInfo.list = list;
|
||||||
|
await saveAppInfo(this.appInfo, this.appsPath, this.configFilename);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 加载配置
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async loadConfig(loadApps = false) {
|
||||||
|
if (loadApps) {
|
||||||
|
return await this.load();
|
||||||
|
}
|
||||||
|
const appInfos = await loadAppInfo(this.appsPath, this.configFilename);
|
||||||
|
this.appInfo = appInfos;
|
||||||
|
const list = (appInfos?.list || []) as T[];
|
||||||
|
for (const app of list) {
|
||||||
|
this.apps.set(app.key, app);
|
||||||
|
}
|
||||||
|
return this.apps;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 初始化应用的时候加载
|
||||||
|
*/
|
||||||
|
async load(opts?: LoadOptions) {
|
||||||
|
// 从apps文件夹列表当中中加载app信息
|
||||||
|
if (opts?.configFilename) {
|
||||||
|
this.configFilename = opts.configFilename;
|
||||||
|
}
|
||||||
|
if (opts?.appsPath) {
|
||||||
|
this.appsPath = opts.appsPath;
|
||||||
|
}
|
||||||
|
if (opts?.mainApp) {
|
||||||
|
this.mainApp = opts.mainApp;
|
||||||
|
}
|
||||||
|
const appInfos = await loadAppInfo(this.appsPath, this.configFilename);
|
||||||
|
this.appInfo = appInfos;
|
||||||
|
const _List = (appInfos?.list as T[]) || [];
|
||||||
|
const list = _List.filter((item) => {
|
||||||
|
if (opts?.runtime && item.runtime) {
|
||||||
|
return item.runtime?.includes(opts.runtime);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
const pm2Connect = new Pm2Connect(false);
|
||||||
|
this.#pm2Connect = pm2Connect;
|
||||||
|
try {
|
||||||
|
await pm2Connect.connect();
|
||||||
|
} catch (e) {
|
||||||
|
console.log('pm2 connect error', e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const app of list) {
|
||||||
|
try {
|
||||||
|
const loaded = await this.loadApp(app);
|
||||||
|
if (!loaded) {
|
||||||
|
// 加载失败,如果是running状态,设置为error
|
||||||
|
if (app.status === 'running') {
|
||||||
|
app.status = 'error';
|
||||||
|
console.log('load app error', app); // save app error info
|
||||||
|
await this.saveAppInfo(app);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// console.log('load app success', app);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`load app error====[${app.type} | ${app.key}]\n`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pm2Connect.disconnect();
|
||||||
|
this.#pm2Connect = null;
|
||||||
|
}
|
||||||
|
async add(app: T) {
|
||||||
|
if (this.checkKey(app.key)) {
|
||||||
|
console.error('key is loaded');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.saveAppInfo(app, true);
|
||||||
|
this.loadApp(app);
|
||||||
|
}
|
||||||
|
// 启动
|
||||||
|
async start(key: string) {
|
||||||
|
const app = this.apps.get(key);
|
||||||
|
if (!app) {
|
||||||
|
console.error('app not found', key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (app.status === 'running' && app.type === AppType.SystemApp) {
|
||||||
|
console.log(`app ${key} is running`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
app.status = 'running';
|
||||||
|
this.loadApp(app);
|
||||||
|
await this.saveAppInfo(app);
|
||||||
|
}
|
||||||
|
// 停止
|
||||||
|
async stop(key: string) {
|
||||||
|
const app = this.apps.get(key);
|
||||||
|
if (!app) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (app.status === 'stop' && app.type === AppType.SystemApp) {
|
||||||
|
console.log(`app ${key} is stopped`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
app.status = 'stop';
|
||||||
|
if (app.type === AppType.MicroApp) {
|
||||||
|
if (app.process) {
|
||||||
|
app.process.kill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (app.type === AppType.Pm2SystemApp) {
|
||||||
|
const pm2Manager = new Pm2Manager({
|
||||||
|
appName: app.key,
|
||||||
|
script: app.entry,
|
||||||
|
pm2Connect: this.#pm2Connect
|
||||||
|
});
|
||||||
|
await pm2Manager.stop();
|
||||||
|
}
|
||||||
|
await this.saveAppInfo(app);
|
||||||
|
}
|
||||||
|
async restart(key: string) {
|
||||||
|
const app = this.apps.get(key);
|
||||||
|
if (!app) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (app.status !== 'running') {
|
||||||
|
await this.start(key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (app.status === 'running' && app.type === AppType.Pm2SystemApp) {
|
||||||
|
const pm2Manager = new Pm2Manager({
|
||||||
|
appName: app.key
|
||||||
|
});
|
||||||
|
await pm2Manager.restart();
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// 重新启动
|
||||||
|
await this.stop(key);
|
||||||
|
await this.start(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取app信息, 用于展示
|
||||||
|
* @param key
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
getAppShowInfo(key: string) {
|
||||||
|
const app = this.apps.get(key);
|
||||||
|
if (!app) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return onAppShowInfo(app);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取所有app信息, 用于展示
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
getAllAppShowInfo() {
|
||||||
|
const list = [];
|
||||||
|
for (const [key, value] of this.apps) {
|
||||||
|
list.push(onAppShowInfo(value));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 更新app信息, 用于展示, 加上一些功能,启动,停止程序
|
||||||
|
* @param key
|
||||||
|
* @param info
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async updateAppInfo(key: string, info: Partial<T>) {
|
||||||
|
const app = this.apps.get(key);
|
||||||
|
if (!app) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
merge(app, info);
|
||||||
|
this.loadApp(app);
|
||||||
|
await this.saveAppInfo(app);
|
||||||
|
return onAppShowInfo(app);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 删除app信息
|
||||||
|
* @param key
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async removeApp(key: string) {
|
||||||
|
const app = this.apps.get(key);
|
||||||
|
if (!app) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (app.process) {
|
||||||
|
app.process.kill();
|
||||||
|
}
|
||||||
|
this.apps.delete(key);
|
||||||
|
this.appInfo.list = this.appInfo.list.filter((item) => item.key !== key);
|
||||||
|
await saveAppInfo(this.appInfo, this.appsPath, this.configFilename);
|
||||||
|
try {
|
||||||
|
if (app.type === AppType.Pm2SystemApp) {
|
||||||
|
const pm2Manager = new Pm2Manager({
|
||||||
|
appName: app.key
|
||||||
|
});
|
||||||
|
await pm2Manager.deleteProcess();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('delete pm2 process error', e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
deleteFileAppInfo(key, this.appsPath);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('delete file app error', e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Detect micro app,检测apps的没有加载进来的app模块
|
||||||
|
* @param manager
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async detectApp(opts: { autoClear?: boolean } = {}) {
|
||||||
|
const manager = this;
|
||||||
|
const { autoClear = true } = opts;
|
||||||
|
const list = manager.getAllAppShowInfo();
|
||||||
|
const appPathKeys = await getAppPathKeys(this.appsPath);
|
||||||
|
let hasDeletedList: any[] = [];
|
||||||
|
console.log('App path keys', appPathKeys);
|
||||||
|
if (autoClear) {
|
||||||
|
hasDeletedList = list.filter((item) => !appPathKeys.find((key) => item.key === key));
|
||||||
|
console.log('Has deleted', hasDeletedList);
|
||||||
|
for (const item of hasDeletedList) {
|
||||||
|
try {
|
||||||
|
await manager.removeApp(item.key);
|
||||||
|
} catch (e) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const notIn = appPathKeys.filter((key) => !list.find((item) => item.key === key));
|
||||||
|
console.log('Not in', notIn);
|
||||||
|
const loadInfo = [];
|
||||||
|
if (notIn.length <= 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (const key of notIn) {
|
||||||
|
try {
|
||||||
|
const { showAppInfo } = await installAppFromKey(key, this.appsPath);
|
||||||
|
await manager.add(showAppInfo as any);
|
||||||
|
loadInfo.push(`Load ${key} success`);
|
||||||
|
} catch (e) {
|
||||||
|
loadInfo.push(`Load ${key} error:`, e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { list: loadInfo, newApps: notIn, hasDeletedList };
|
||||||
|
}
|
||||||
|
async reload(key: string) {
|
||||||
|
const manager = this;
|
||||||
|
const app = this.apps.get(key);
|
||||||
|
if (!app) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const appStatus = app.status;
|
||||||
|
try {
|
||||||
|
await manager.removeApp(key);
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { showAppInfo } = await installAppFromKey(key, this.appsPath);
|
||||||
|
await manager.add(showAppInfo as any);
|
||||||
|
if (appStatus === 'running') {
|
||||||
|
await manager.start(key);
|
||||||
|
}
|
||||||
|
console.log('reload app success', key);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('reload app error', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安装app通过key
|
||||||
|
* @param key
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const installAppFromKey = async (key: string, _appPath: string) => {
|
||||||
|
// 标准化key中的路径分隔符,统一使用系统路径分隔符
|
||||||
|
const normalizedKey = key.replace(/\//g, path.sep);
|
||||||
|
const directory = path.join(_appPath, normalizedKey);
|
||||||
|
if (!fileIsExist(directory)) {
|
||||||
|
console.error('App not found', directory);
|
||||||
|
throw new Error('App not found');
|
||||||
|
}
|
||||||
|
const pkgs = path.join(directory, 'package.json');
|
||||||
|
if (!fileIsExist(pkgs)) {
|
||||||
|
throw new Error('Invalid package.json, need package.json in app directory');
|
||||||
|
}
|
||||||
|
const json = fs.readFileSync(pkgs, 'utf-8');
|
||||||
|
const pkg = JSON.parse(json);
|
||||||
|
const { name, version, app } = pkg;
|
||||||
|
if (!name || !version || !app) {
|
||||||
|
console.error('need name, version and app in package.json');
|
||||||
|
throw new Error('Invalid package.json format, need name, version and app');
|
||||||
|
}
|
||||||
|
const readmeFile = path.join(directory, 'README.md');
|
||||||
|
const readmeFile2 = path.join(directory, 'readme.md');
|
||||||
|
let readmeDesc = '';
|
||||||
|
if (fileIsExist(readmeFile)) {
|
||||||
|
readmeDesc = fs.readFileSync(readmeFile, 'utf-8');
|
||||||
|
} else if (fileIsExist(readmeFile2)) {
|
||||||
|
readmeDesc = fs.readFileSync(readmeFile2, 'utf-8');
|
||||||
|
}
|
||||||
|
let showAppInfo: AppInfo = {
|
||||||
|
key,
|
||||||
|
status: 'inactive',
|
||||||
|
type: app?.type || 'system-app',
|
||||||
|
description: app?.description || pkg?.description || readmeDesc || '',
|
||||||
|
version,
|
||||||
|
runtime: app?.runtime || [],
|
||||||
|
//
|
||||||
|
entry: app?.entry || '',
|
||||||
|
path: directory,
|
||||||
|
origin: app
|
||||||
|
};
|
||||||
|
app.key = key;
|
||||||
|
if (app.type === 'pm2-system-app') {
|
||||||
|
const pm2Cwd = path.join(_appPath, '..');
|
||||||
|
showAppInfo.pm2Options = { cwd: pm2Cwd, ...app.pm2Options };
|
||||||
|
}
|
||||||
|
return { pkg, showAppInfo };
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 读取apps文件夹下的所有文件夹,对filename进行过滤
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getAppPathKeys = async (_appPath: string) => {
|
||||||
|
const directory = path.resolve(_appPath);
|
||||||
|
const root = directory;
|
||||||
|
// 使用 posix 风格路径用于 glob 模式,确保跨平台兼容
|
||||||
|
const path1 = '*/package.json';
|
||||||
|
const path2 = '*/*/package.json';
|
||||||
|
const appsPackages = await glob([path1, path2], {
|
||||||
|
cwd: root,
|
||||||
|
onlyFiles: true,
|
||||||
|
absolute: false,
|
||||||
|
ignore: ['**/node_modules/**']
|
||||||
|
});
|
||||||
|
const appPathKeys = appsPackages.map((pkg) => {
|
||||||
|
const dir = path.dirname(pkg);
|
||||||
|
// 直接使用dirname的结果,因为glob已经返回相对于root的路径
|
||||||
|
// 只需要标准化路径分隔符
|
||||||
|
return dir.replace(/\\/g, '/');
|
||||||
|
});
|
||||||
|
return appPathKeys;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
export const clearMicroApp = (link: string) => {
|
||||||
|
try {
|
||||||
|
const moduleUrl = new URL(link, import.meta.url);
|
||||||
|
// 使用 fileURLToPath 确保 Windows 和 Unix 路径兼容
|
||||||
|
const modulePath = fileURLToPath(moduleUrl);
|
||||||
|
delete require.cache[modulePath];
|
||||||
|
console.log(`Module ${link} has been unloaded.`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to unload module ${link}:`, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
243
assistant/src/module/local-apps/src/modules/pm2.ts
Normal file
243
assistant/src/module/local-apps/src/modules/pm2.ts
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
import { spawn } from 'node:child_process';
|
||||||
|
import pm2, { ProcessDescription, StartOptions } from 'pm2';
|
||||||
|
import { promisify } from 'node:util';
|
||||||
|
import type { AppInfo } from './manager.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规范化脚本路径,确保在 Windows 平台上兼容 PM2
|
||||||
|
* @param scriptPath 脚本路径
|
||||||
|
* @returns 规范化后的路径
|
||||||
|
*/
|
||||||
|
export const normalizeScriptPath = (scriptPath: string): string => {
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
// 在 Windows 上,将反斜杠转换为正斜杠,PM2 更好地支持正斜杠
|
||||||
|
return scriptPath.replace(/\\/g, '/');
|
||||||
|
}
|
||||||
|
return scriptPath;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const connect = async (noDaemonMode: boolean = false) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
pm2.connect(noDaemonMode, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('pm2 connect error', err);
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
export const disconnect = promisify(pm2.disconnect).bind(pm2);
|
||||||
|
export const start = promisify(pm2.start).bind(pm2);
|
||||||
|
export const stop = promisify(pm2.stop).bind(pm2);
|
||||||
|
export const restart = promisify(pm2.restart).bind(pm2);
|
||||||
|
export const reload = promisify(pm2.reload).bind(pm2);
|
||||||
|
const deleteProcess = promisify(pm2.delete).bind(pm2);
|
||||||
|
const list = promisify(pm2.list).bind(pm2);
|
||||||
|
export type Pm2Opts = {
|
||||||
|
appName: string;
|
||||||
|
script?: string;
|
||||||
|
pm2Connect?: Pm2Connect;
|
||||||
|
/**
|
||||||
|
* 启动程序的路径,比如node deno bun interpreter
|
||||||
|
*/
|
||||||
|
interpreter?: string;
|
||||||
|
env?: Record<string, any>;
|
||||||
|
};
|
||||||
|
export const checkInstall = async (app: AppInfo) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// 启动前,先执行pnpm install , 如果失败,则不启动
|
||||||
|
const install = spawn('pnpm', ['install'], {
|
||||||
|
cwd: app.path,
|
||||||
|
stdio: 'inherit'
|
||||||
|
});
|
||||||
|
install.on('close', (code) => {
|
||||||
|
if (code !== 0) {
|
||||||
|
console.log('install failed');
|
||||||
|
return resolve(false);
|
||||||
|
}
|
||||||
|
console.log('install success');
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
export class Pm2Connect {
|
||||||
|
needConnect = true;
|
||||||
|
isConnected = false;
|
||||||
|
constructor(needConnect = true) {
|
||||||
|
this.needConnect = needConnect;
|
||||||
|
}
|
||||||
|
async sleep(ms: number) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, ms);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async checkConnect() {
|
||||||
|
const that = this;
|
||||||
|
try {
|
||||||
|
if (this.needConnect && !this.isConnected) {
|
||||||
|
const data = await connect();
|
||||||
|
that.isConnected = !!data;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('pm2 check connect error', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async checkDisconnect(runOpts?: RunOptions) {
|
||||||
|
const needExit = runOpts?.needExit ?? true;
|
||||||
|
if (this.needConnect && this.isConnected && needExit) {
|
||||||
|
this.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async connect() {
|
||||||
|
try {
|
||||||
|
await connect();
|
||||||
|
this.isConnected = true;
|
||||||
|
} catch (e) {
|
||||||
|
console.log('pm2 connect error', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async disconnect() {
|
||||||
|
try {
|
||||||
|
pm2.disconnect();
|
||||||
|
await this.sleep(1000);
|
||||||
|
} catch (e) {
|
||||||
|
console.log('pm2 disconnect error', e);
|
||||||
|
}
|
||||||
|
this.isConnected = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type RunOptions = {
|
||||||
|
needExit?: boolean;
|
||||||
|
};
|
||||||
|
export class Pm2Manager {
|
||||||
|
pm2Connect: Pm2Connect;
|
||||||
|
/**
|
||||||
|
* app name
|
||||||
|
*/
|
||||||
|
appName: string;
|
||||||
|
/**
|
||||||
|
* 启动脚本的路径
|
||||||
|
*/
|
||||||
|
script?: string;
|
||||||
|
/**
|
||||||
|
* 启动脚本的路径
|
||||||
|
*/
|
||||||
|
interpreter?: string;
|
||||||
|
/**
|
||||||
|
* 批量更新的时候不需要一直connect和关闭
|
||||||
|
*/
|
||||||
|
needConnect = true;
|
||||||
|
isConnect = false;
|
||||||
|
env: any;
|
||||||
|
constructor(opts: Pm2Opts) {
|
||||||
|
this.appName = opts.appName;
|
||||||
|
this.script = opts.script;
|
||||||
|
this.interpreter = opts.interpreter;
|
||||||
|
this.pm2Connect = opts.pm2Connect || new Pm2Connect();
|
||||||
|
this.env = opts.env || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
async list(runOpts?: RunOptions): Promise<ProcessDescription[]> {
|
||||||
|
const _runOpts = { needExit: false, ...runOpts };
|
||||||
|
try {
|
||||||
|
await this.pm2Connect.checkConnect();
|
||||||
|
const apps = await list();
|
||||||
|
return apps;
|
||||||
|
} catch (e) {
|
||||||
|
console.log('pm2 run error', e);
|
||||||
|
return [];
|
||||||
|
} finally {
|
||||||
|
this.pm2Connect.checkDisconnect(_runOpts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 启动
|
||||||
|
*/
|
||||||
|
async start(options?: StartOptions, runOpts?: RunOptions) {
|
||||||
|
const { appName, script, env, interpreter } = this;
|
||||||
|
const needExit = runOpts?.needExit ?? true;
|
||||||
|
if (!script) {
|
||||||
|
console.error('script is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 规范化脚本路径以兼容 Windows
|
||||||
|
const normalizedScript = normalizeScriptPath(script);
|
||||||
|
const starter: StartOptions = {
|
||||||
|
name: appName,
|
||||||
|
script: normalizedScript, // examples: ./agent/main.ts
|
||||||
|
// execute_command: execPath,
|
||||||
|
cwd: process.cwd(),
|
||||||
|
interpreter: interpreter,
|
||||||
|
...options,
|
||||||
|
env: {
|
||||||
|
NODE_ENV: 'production',
|
||||||
|
...env,
|
||||||
|
...options?.env
|
||||||
|
}
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await this.pm2Connect.checkConnect();
|
||||||
|
const apps = await this.list({ needExit: false });
|
||||||
|
const app = apps.find((app) => app.name === appName);
|
||||||
|
if (app && app.pid === 0) {
|
||||||
|
await start(starter);
|
||||||
|
return;
|
||||||
|
} else if (!app) {
|
||||||
|
await start(starter);
|
||||||
|
} else {
|
||||||
|
console.log(`pm2 app ${appName} is running`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('error', e);
|
||||||
|
} finally {
|
||||||
|
this.pm2Connect.checkDisconnect({ needExit });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止
|
||||||
|
*/
|
||||||
|
async stop(runOpts?: RunOptions) {
|
||||||
|
try {
|
||||||
|
await this.pm2Connect.checkConnect();
|
||||||
|
const apps = await this.list({ needExit: false });
|
||||||
|
const app = apps.find((app) => app.name === this.appName);
|
||||||
|
if (app && app.pid !== 0) {
|
||||||
|
await stop(app.name);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('error', e);
|
||||||
|
} finally {
|
||||||
|
this.pm2Connect.checkDisconnect(runOpts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async restart(runOpts?: RunOptions) {
|
||||||
|
try {
|
||||||
|
await this.pm2Connect.checkConnect();
|
||||||
|
const apps = await this.list({ needExit: false });
|
||||||
|
const app = apps.find((app) => app.name === this.appName);
|
||||||
|
if (app) {
|
||||||
|
await restart(app.name);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('error', e);
|
||||||
|
} finally {
|
||||||
|
this.pm2Connect.checkDisconnect(runOpts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async deleteProcess(runOpts?: RunOptions) {
|
||||||
|
try {
|
||||||
|
await this.pm2Connect.checkConnect();
|
||||||
|
const apps = await this.list({ needExit: false });
|
||||||
|
const app = apps.find((app) => app.name === this.appName);
|
||||||
|
if (app) {
|
||||||
|
await deleteProcess(app.name);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('error', e);
|
||||||
|
} finally {
|
||||||
|
this.pm2Connect.checkDisconnect(runOpts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
115
assistant/src/module/local-apps/src/routes/list.ts
Normal file
115
assistant/src/module/local-apps/src/routes/list.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { app } from '../app.ts';
|
||||||
|
import { manager } from '../manager.ts';
|
||||||
|
|
||||||
|
app
|
||||||
|
.route({
|
||||||
|
path: 'local-apps',
|
||||||
|
key: 'detect',
|
||||||
|
description: 'Detect local apps',
|
||||||
|
middleware: ['auth-admin']
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
const res = await manager.detectApp();
|
||||||
|
ctx.body = res;
|
||||||
|
})
|
||||||
|
.addTo(app);
|
||||||
|
|
||||||
|
app
|
||||||
|
.route({
|
||||||
|
path: 'local-apps',
|
||||||
|
key: 'list',
|
||||||
|
middleware: ['auth-admin'],
|
||||||
|
description: 'List local apps'
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
const list = manager.getAllAppShowInfo();
|
||||||
|
ctx.body = list;
|
||||||
|
})
|
||||||
|
.addTo(app);
|
||||||
|
|
||||||
|
app
|
||||||
|
.route({
|
||||||
|
path: 'local-apps',
|
||||||
|
key: 'updateStatus',
|
||||||
|
middleware: ['auth-admin'],
|
||||||
|
description: 'Update app status, start or stop, parmas: status, appKey'
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
const { status, appKey } = ctx.query;
|
||||||
|
if (!status || !appKey) {
|
||||||
|
ctx.body = 'status or key is required';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const app = manager.apps.get(appKey);
|
||||||
|
if (!app) {
|
||||||
|
ctx.throw(404, 'App not found');
|
||||||
|
}
|
||||||
|
if (status === 'start') {
|
||||||
|
await manager.start(appKey);
|
||||||
|
} else if (status === 'stop') {
|
||||||
|
await manager.stop(appKey);
|
||||||
|
}
|
||||||
|
const appShow = manager.getAppShowInfo(appKey);
|
||||||
|
ctx.body = appShow;
|
||||||
|
})
|
||||||
|
.addTo(app);
|
||||||
|
|
||||||
|
app
|
||||||
|
.route({
|
||||||
|
path: 'local-apps',
|
||||||
|
key: 'operate',
|
||||||
|
middleware: ['auth-admin'],
|
||||||
|
description: 'Operate app, parmas: appKey, action: start, stop, restart, removeApp, reload'
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
const { appKey, action } = ctx.query;
|
||||||
|
if (!appKey || !action) {
|
||||||
|
ctx.throw(400, 'appKey or action is required');
|
||||||
|
}
|
||||||
|
if (!['start', 'stop', 'restart', 'removeApp', 'reload'].includes(action)) {
|
||||||
|
ctx.throw(400, 'action is invalid');
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = manager.apps.get(appKey);
|
||||||
|
if (!app) {
|
||||||
|
ctx.throw(404, 'App not found');
|
||||||
|
}
|
||||||
|
await manager[action](appKey);
|
||||||
|
const appShow = manager.getAppShowInfo(appKey);
|
||||||
|
ctx.body = appShow;
|
||||||
|
})
|
||||||
|
.addTo(app);
|
||||||
|
|
||||||
|
app
|
||||||
|
.route({
|
||||||
|
path: 'local-apps',
|
||||||
|
key: 'update',
|
||||||
|
middleware: ['auth-admin'],
|
||||||
|
description: 'Update app info'
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
const { key } = ctx.query.data || {};
|
||||||
|
if (!key) {
|
||||||
|
ctx.body = 'key is required';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const appInfo = await manager.updateAppInfo(key, ctx.query.data);
|
||||||
|
ctx.body = appInfo;
|
||||||
|
})
|
||||||
|
.addTo(app);
|
||||||
|
|
||||||
|
app
|
||||||
|
.route({
|
||||||
|
path: 'local-apps',
|
||||||
|
key: 'delete',
|
||||||
|
middleware: ['auth-admin']
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
const { appKey } = ctx.query;
|
||||||
|
if (!appKey) {
|
||||||
|
ctx.throw(400, 'key is required');
|
||||||
|
}
|
||||||
|
const res = await manager.removeApp(appKey);
|
||||||
|
ctx.body = res ? 'ok' : 'fail';
|
||||||
|
})
|
||||||
|
.addTo(app);
|
||||||
31
assistant/src/module/local-apps/src/uitls/npm.ts
Normal file
31
assistant/src/module/local-apps/src/uitls/npm.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { spawn, spawnSync } from 'node:child_process';
|
||||||
|
|
||||||
|
export const checkPnpm = () => {
|
||||||
|
try {
|
||||||
|
spawnSync('pnpm', ['--version']);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
type InstallDepsOptions = {
|
||||||
|
appPath: string;
|
||||||
|
isProduction?: boolean;
|
||||||
|
sync?: boolean;
|
||||||
|
};
|
||||||
|
export const installDeps = (opts: InstallDepsOptions) => {
|
||||||
|
const { appPath } = opts;
|
||||||
|
const isProduction = opts.isProduction ?? true;
|
||||||
|
const params = ['i'];
|
||||||
|
if (isProduction) {
|
||||||
|
params.push('--production');
|
||||||
|
}
|
||||||
|
console.log('installDeps', appPath, params);
|
||||||
|
const syncSpawn = opts.sync ? spawnSync : spawn;
|
||||||
|
if (checkPnpm()) {
|
||||||
|
syncSpawn('pnpm', params, { cwd: appPath, stdio: 'inherit', env: process.env });
|
||||||
|
} else {
|
||||||
|
syncSpawn('npm', params, { cwd: appPath, stdio: 'inherit', env: process.env });
|
||||||
|
}
|
||||||
|
};
|
||||||
11
assistant/src/module/local-apps/test/deno.ts
Normal file
11
assistant/src/module/local-apps/test/deno.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
console.log('Hello, world!');
|
||||||
|
import http from 'node:http';
|
||||||
|
// @ts-ignore
|
||||||
|
const isDeno = typeof Deno !== 'undefined' && Deno?.version?.deno;
|
||||||
|
console.log('isDeno', isDeno);
|
||||||
|
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
res.end('Hello, world!');
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(8010);
|
||||||
13
assistant/src/module/local-apps/test/detect.ts
Normal file
13
assistant/src/module/local-apps/test/detect.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// import { manager } from '../src/manager';
|
||||||
|
import { Manager } from '../src/modules/manager.ts';
|
||||||
|
|
||||||
|
export const manager = new Manager({
|
||||||
|
// mainApp: app
|
||||||
|
});
|
||||||
|
const main = async () => {
|
||||||
|
await manager.loadConfig();
|
||||||
|
const res = await manager.detectApp();
|
||||||
|
console.log(res);
|
||||||
|
};
|
||||||
|
|
||||||
|
main();
|
||||||
7
assistant/src/module/local-apps/test/list.ts
Normal file
7
assistant/src/module/local-apps/test/list.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { manager } from '../src/manager';
|
||||||
|
const main = async () => {
|
||||||
|
const res = manager.getAllAppShowInfo();
|
||||||
|
console.log(res);
|
||||||
|
};
|
||||||
|
|
||||||
|
main();
|
||||||
19
assistant/src/module/local-apps/test/load-app.ts
Normal file
19
assistant/src/module/local-apps/test/load-app.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { loadAppInfo } from '../src/modules/app-file';
|
||||||
|
import { Manager } from '../src/modules/manager.ts';
|
||||||
|
|
||||||
|
|
||||||
|
const main = async () => {
|
||||||
|
const appInfo = await loadAppInfo("apps");
|
||||||
|
console.log(appInfo);
|
||||||
|
};
|
||||||
|
// main();
|
||||||
|
|
||||||
|
const start = async () => {
|
||||||
|
const manager = new Manager({
|
||||||
|
// mainApp: app
|
||||||
|
});
|
||||||
|
await manager.loadConfig()
|
||||||
|
await manager.start('root/test-log')
|
||||||
|
}
|
||||||
|
|
||||||
|
start()
|
||||||
21
assistant/src/module/local-apps/test/pm2-start-test.ts
Normal file
21
assistant/src/module/local-apps/test/pm2-start-test.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Pm2Manager, connect, disconnect } from '../src/modules/pm2.ts';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pm2 = new Pm2Manager({
|
||||||
|
appName: 'test2',
|
||||||
|
script: 'test/deno.ts',
|
||||||
|
interpreter: 'deno'
|
||||||
|
});
|
||||||
|
const main = async () => {
|
||||||
|
console.log('pm2 start test2');
|
||||||
|
await pm2.start({
|
||||||
|
// args: ['--allow-net']
|
||||||
|
interpreter_args: ['-A']
|
||||||
|
});
|
||||||
|
console.log('pm2 disconnect test2');
|
||||||
|
console.log('pm2', pm2.isConnect);
|
||||||
|
};
|
||||||
|
main();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('err', e);
|
||||||
|
}
|
||||||
26
assistant/src/module/local-apps/test/pm2.ts
Normal file
26
assistant/src/module/local-apps/test/pm2.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Pm2Manager } from '../src/modules/pm2';
|
||||||
|
|
||||||
|
const manager = new Pm2Manager({
|
||||||
|
appName: 'test',
|
||||||
|
script: 'test.js'
|
||||||
|
});
|
||||||
|
|
||||||
|
const main = async () => {
|
||||||
|
const list = await manager.list();
|
||||||
|
console.log('list', list);
|
||||||
|
// manager.restart()
|
||||||
|
};
|
||||||
|
|
||||||
|
// main();
|
||||||
|
|
||||||
|
const pm2Demo = new Pm2Manager({
|
||||||
|
appName: 'pm2-demo'
|
||||||
|
});
|
||||||
|
|
||||||
|
const pm2Restart = async () => {
|
||||||
|
const list = await pm2Demo.list();
|
||||||
|
console.log('list', list);
|
||||||
|
pm2Demo.restart()
|
||||||
|
}
|
||||||
|
|
||||||
|
pm2Restart();
|
||||||
16
assistant/src/module/local-apps/test/start-test-ts.ts
Normal file
16
assistant/src/module/local-apps/test/start-test-ts.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
// import { manager } from '../src/manager';
|
||||||
|
import { Manager } from '../src/modules/manager.ts';
|
||||||
|
|
||||||
|
export const manager = new Manager({
|
||||||
|
// mainApp: app
|
||||||
|
});
|
||||||
|
const main = async () => {
|
||||||
|
await manager.loadConfig(true);
|
||||||
|
// const res = await manager.detectApp();
|
||||||
|
// console.log(res);
|
||||||
|
const res = await manager.start('root/test-log');
|
||||||
|
// const res = await manager.stop('root/test-log');
|
||||||
|
console.log(res);
|
||||||
|
};
|
||||||
|
|
||||||
|
main();
|
||||||
33
assistant/src/module/local-apps/tsconfig.json
Normal file
33
assistant/src/module/local-apps/tsconfig.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "nodenext",
|
||||||
|
"target": "esnext",
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"sourceMap": false,
|
||||||
|
"allowJs": true,
|
||||||
|
"newLine": "LF",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"typeRoots": [
|
||||||
|
"node_modules/@types",
|
||||||
|
"node_modules/@kevisual/types"
|
||||||
|
],
|
||||||
|
"declaration": true,
|
||||||
|
"noEmit": false,
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"src/*"
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"typings.d.ts",
|
||||||
|
"src/**/*.ts",
|
||||||
|
],
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user