diff --git a/assistant/src/module/local-apps/.gitignore b/assistant/src/module/local-apps/.gitignore new file mode 100644 index 0000000..3100c29 --- /dev/null +++ b/assistant/src/module/local-apps/.gitignore @@ -0,0 +1,7 @@ +node_modules +/apps +dist +/logs + +/app.config.json5 +/app.config.json \ No newline at end of file diff --git a/assistant/src/module/local-apps/.npmrc b/assistant/src/module/local-apps/.npmrc new file mode 100644 index 0000000..a4d9caf --- /dev/null +++ b/assistant/src/module/local-apps/.npmrc @@ -0,0 +1,3 @@ +//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN} +@abearxiong:registry=https://npm.pkg.github.com +//registry.npmjs.org/:_authToken=${NPM_TOKEN} \ No newline at end of file diff --git a/assistant/src/module/local-apps/apps.config.json b/assistant/src/module/local-apps/apps.config.json new file mode 100644 index 0000000..3a12e38 --- /dev/null +++ b/assistant/src/module/local-apps/apps.config.json @@ -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 + } + ] +} \ No newline at end of file diff --git a/assistant/src/module/local-apps/bun.config.ts b/assistant/src/module/local-apps/bun.config.ts new file mode 100644 index 0000000..5f6782d --- /dev/null +++ b/assistant/src/module/local-apps/bun.config.ts @@ -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.'); \ No newline at end of file diff --git a/assistant/src/module/local-apps/demos/demo/demo-dist/app.mjs b/assistant/src/module/local-apps/demos/demo/demo-dist/app.mjs new file mode 100644 index 0000000..926beae --- /dev/null +++ b/assistant/src/module/local-apps/demos/demo/demo-dist/app.mjs @@ -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 }; diff --git a/assistant/src/module/local-apps/demos/demo/package.json b/assistant/src/module/local-apps/demos/demo/package.json new file mode 100644 index 0000000..6b62e59 --- /dev/null +++ b/assistant/src/module/local-apps/demos/demo/package.json @@ -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 ", + "license": "MIT", + "type": "module" +} \ No newline at end of file diff --git a/assistant/src/module/local-apps/demos/root/test-log/main.ts b/assistant/src/module/local-apps/demos/root/test-log/main.ts new file mode 100644 index 0000000..12b0f28 --- /dev/null +++ b/assistant/src/module/local-apps/demos/root/test-log/main.ts @@ -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.'); diff --git a/assistant/src/module/local-apps/demos/root/test-log/package.json b/assistant/src/module/local-apps/demos/root/test-log/package.json new file mode 100644 index 0000000..f2c479c --- /dev/null +++ b/assistant/src/module/local-apps/demos/root/test-log/package.json @@ -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": [] +} \ No newline at end of file diff --git a/assistant/src/module/local-apps/demos/root/test-no/main.ts b/assistant/src/module/local-apps/demos/root/test-no/main.ts new file mode 100644 index 0000000..12b0f28 --- /dev/null +++ b/assistant/src/module/local-apps/demos/root/test-no/main.ts @@ -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.'); diff --git a/assistant/src/module/local-apps/demos/root/test-no/package.json b/assistant/src/module/local-apps/demos/root/test-no/package.json new file mode 100644 index 0000000..33ad7e1 --- /dev/null +++ b/assistant/src/module/local-apps/demos/root/test-no/package.json @@ -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 (https://www.xiongxiao.me)", + "license": "MIT", + "type": "module" +} \ No newline at end of file diff --git a/assistant/src/module/local-apps/package.json b/assistant/src/module/local-apps/package.json new file mode 100644 index 0000000..04aee12 --- /dev/null +++ b/assistant/src/module/local-apps/package.json @@ -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 ", + "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" + } +} \ No newline at end of file diff --git a/assistant/src/module/local-apps/readme.md b/assistant/src/module/local-apps/readme.md new file mode 100644 index 0000000..930f26c --- /dev/null +++ b/assistant/src/module/local-apps/readme.md @@ -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; // 原始数据 + entry?: string; // 入口文件 + path?: string; // 文件路径 + env?: Record; // 环境变量 + 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; +} +``` diff --git a/assistant/src/module/local-apps/src/app.ts b/assistant/src/module/local-apps/src/app.ts new file mode 100644 index 0000000..a265c26 --- /dev/null +++ b/assistant/src/module/local-apps/src/app.ts @@ -0,0 +1,3 @@ +import { App } from '@kevisual/router'; +import { useContextKey } from '@kevisual/use-config/context'; +export const app = useContextKey('app', () => new App()); diff --git a/assistant/src/module/local-apps/src/index.ts b/assistant/src/module/local-apps/src/index.ts new file mode 100644 index 0000000..aab9818 --- /dev/null +++ b/assistant/src/module/local-apps/src/index.ts @@ -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) => { + // +}; diff --git a/assistant/src/module/local-apps/src/manager.ts b/assistant/src/module/local-apps/src/manager.ts new file mode 100644 index 0000000..fcc2f37 --- /dev/null +++ b/assistant/src/module/local-apps/src/manager.ts @@ -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); +}; diff --git a/assistant/src/module/local-apps/src/modules/app-file.ts b/assistant/src/module/local-apps/src/modules/app-file.ts new file mode 100644 index 0000000..8c4d201 --- /dev/null +++ b/assistant/src/module/local-apps/src/modules/app-file.ts @@ -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 => { + 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 }); +}; diff --git a/assistant/src/module/local-apps/src/modules/manager.ts b/assistant/src/module/local-apps/src/modules/manager.ts new file mode 100644 index 0000000..2396224 --- /dev/null +++ b/assistant/src/module/local-apps/src/modules/manager.ts @@ -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; // 原始数据 + entry?: string; // 入口文件 + path?: string; // 文件路径 + env?: Record; // 环境变量 + 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 { + apps: Map; + 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) { + 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); + } +}; diff --git a/assistant/src/module/local-apps/src/modules/pm2.ts b/assistant/src/module/local-apps/src/modules/pm2.ts new file mode 100644 index 0000000..ef2fd1c --- /dev/null +++ b/assistant/src/module/local-apps/src/modules/pm2.ts @@ -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; +}; +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 { + 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); + } + } +} diff --git a/assistant/src/module/local-apps/src/routes/list.ts b/assistant/src/module/local-apps/src/routes/list.ts new file mode 100644 index 0000000..3427e86 --- /dev/null +++ b/assistant/src/module/local-apps/src/routes/list.ts @@ -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); diff --git a/assistant/src/module/local-apps/src/uitls/npm.ts b/assistant/src/module/local-apps/src/uitls/npm.ts new file mode 100644 index 0000000..8571bd5 --- /dev/null +++ b/assistant/src/module/local-apps/src/uitls/npm.ts @@ -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 }); + } +}; diff --git a/assistant/src/module/local-apps/test/deno.ts b/assistant/src/module/local-apps/test/deno.ts new file mode 100644 index 0000000..1d697c4 --- /dev/null +++ b/assistant/src/module/local-apps/test/deno.ts @@ -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); \ No newline at end of file diff --git a/assistant/src/module/local-apps/test/detect.ts b/assistant/src/module/local-apps/test/detect.ts new file mode 100644 index 0000000..44e3ec2 --- /dev/null +++ b/assistant/src/module/local-apps/test/detect.ts @@ -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(); diff --git a/assistant/src/module/local-apps/test/list.ts b/assistant/src/module/local-apps/test/list.ts new file mode 100644 index 0000000..3450f1e --- /dev/null +++ b/assistant/src/module/local-apps/test/list.ts @@ -0,0 +1,7 @@ +import { manager } from '../src/manager'; +const main = async () => { + const res = manager.getAllAppShowInfo(); + console.log(res); +}; + +main(); diff --git a/assistant/src/module/local-apps/test/load-app.ts b/assistant/src/module/local-apps/test/load-app.ts new file mode 100644 index 0000000..dbd1943 --- /dev/null +++ b/assistant/src/module/local-apps/test/load-app.ts @@ -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() \ No newline at end of file diff --git a/assistant/src/module/local-apps/test/pm2-start-test.ts b/assistant/src/module/local-apps/test/pm2-start-test.ts new file mode 100644 index 0000000..1f7d3e7 --- /dev/null +++ b/assistant/src/module/local-apps/test/pm2-start-test.ts @@ -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); +} diff --git a/assistant/src/module/local-apps/test/pm2.ts b/assistant/src/module/local-apps/test/pm2.ts new file mode 100644 index 0000000..3bf3f00 --- /dev/null +++ b/assistant/src/module/local-apps/test/pm2.ts @@ -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(); \ No newline at end of file diff --git a/assistant/src/module/local-apps/test/start-test-ts.ts b/assistant/src/module/local-apps/test/start-test-ts.ts new file mode 100644 index 0000000..b3376fa --- /dev/null +++ b/assistant/src/module/local-apps/test/start-test-ts.ts @@ -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(); diff --git a/assistant/src/module/local-apps/tsconfig.json b/assistant/src/module/local-apps/tsconfig.json new file mode 100644 index 0000000..5ac94b6 --- /dev/null +++ b/assistant/src/module/local-apps/tsconfig.json @@ -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", + ], +} \ No newline at end of file