diff --git a/Dockerfile.rocketadmin-agent b/Dockerfile.rocketadmin-agent index 583e6c2d..c9bddc76 100644 --- a/Dockerfile.rocketadmin-agent +++ b/Dockerfile.rocketadmin-agent @@ -11,7 +11,7 @@ COPY rocketadmin-agent /app/rocketadmin-agent COPY .yarn /app/.yarn RUN yarn install --network-timeout 1000000 RUN cd shared-code && ../node_modules/.bin/tsc -RUN cd rocketadmin-agent && yarn run nest build +RUN cd rocketadmin-agent && yarn run build RUN chown -R appuser:appuser /app diff --git a/backend/src/shared/config/config.service.ts b/backend/src/shared/config/config.service.ts index 197f9be6..6c2ce674 100644 --- a/backend/src/shared/config/config.service.ts +++ b/backend/src/shared/config/config.service.ts @@ -99,7 +99,7 @@ class ConfigService { migrationsRun: false, logging: false, extra: { - max: 2, + max: 10, }, logger: 'advanced-console', driver: pgLiteDriver ? pgLiteDriver : undefined, diff --git a/backend/test/ava-tests/saas-tests/connection-e2e.test.ts b/backend/test/ava-tests/saas-tests/connection-e2e.test.ts index b7f60cb4..3d055c59 100644 --- a/backend/test/ava-tests/saas-tests/connection-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/connection-e2e.test.ts @@ -320,6 +320,8 @@ test(`${currentTest} should return a found connection`, async (t) => { .set('Cookie', token) .set('Accept', 'application/json'); + const foundOneRo = JSON.parse(findOneResponce.text); + console.log('🚀 ~ foundOneRo:', foundOneRo); t.is(findOneResponce.status, 200); const result = findOneResponce.body.connection; @@ -1271,6 +1273,7 @@ test(`${currentTest} throw an exception when group name is not unique`, async (t .set('Accept', 'application/json'); const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); newGroup1.title = 'Admin'; const createGroupResponse = await request(app.getHttpServer()) .post(`/connection/group/${createConnectionRO.id}`) diff --git a/backend/test/utils/register-user-and-return-user-info.ts b/backend/test/utils/register-user-and-return-user-info.ts index deb9984f..2d7c8ad5 100644 --- a/backend/test/utils/register-user-and-return-user-info.ts +++ b/backend/test/utils/register-user-and-return-user-info.ts @@ -19,6 +19,9 @@ export async function registerUserAndReturnUserInfo(app: INestApplication): Prom email: string; password: string; }> { + if (isSaaS()) { + return await registerUserOnSaasAndReturnUserInfo(); + } const dataSource = app.get(BaseType.DATA_SOURCE); const userRepository = dataSource.getRepository(UserEntity); const companyRepository = dataSource.getRepository(CompanyInfoEntity); diff --git a/rocketadmin-agent/Dockerfile b/rocketadmin-agent/Dockerfile index aa667752..699361ea 100644 --- a/rocketadmin-agent/Dockerfile +++ b/rocketadmin-agent/Dockerfile @@ -23,6 +23,6 @@ COPY ./rocketadmin-agent/.clickhouse_test_agent_config.txt /app/rocketadmin-agen COPY shared-code /app/shared-code COPY ./rocketadmin-agent/ssl-cert.txt /app/rocketadmin-agent/ RUN cd shared-code && ../node_modules/.bin/tsc -RUN cd rocketadmin-agent && yarn run nest build +RUN cd rocketadmin-agent && yarn run build COPY ./rocketadmin-agent/wait-for-db2.js /app/ -CMD [ "./node_modules/.bin/nest", "start"] +CMD [ "node", "rocketadmin-agent/dist/main.js"] diff --git a/rocketadmin-agent/package.json b/rocketadmin-agent/package.json index 526f8f9c..70ce5bc5 100644 --- a/rocketadmin-agent/package.json +++ b/rocketadmin-agent/package.json @@ -1,11 +1,12 @@ { "name": "rocketadmin-agent", "version": "0.0.1", - "description": "", - "author": "", + "description": "RocketAdmin Agent - Connect your database to RocketAdmin through a secure agent", + "author": "RocketAdmin", "private": true, "license": "UNLICENSED", "type": "module", + "bin": "./dist/main.js", "packageExtensions": { "ibm_db": { "dependencies": { @@ -15,12 +16,12 @@ }, "scripts": { "prebuild": "rimraf dist", - "build": "nest build", + "build": "tsc -p tsconfig.build.json", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", - "start": "nest start", - "start:dev": "nest start --watch", - "start:debug": "nest start --debug --watch", - "start:prod": "node dist/main", + "start": "node dist/main.js", + "start:dev": "tsc -p tsconfig.build.json && node dist/main.js", + "start:debug": "node --inspect dist/main.js", + "start:prod": "node dist/main.js", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest --runInBand", "test:watch": "jest --watch", @@ -30,44 +31,38 @@ }, "dependencies": { "@azure/core-tracing": "^1.3.1", - "@nestjs/common": "^11.1.8", - "@nestjs/config": "^4.0.2", - "@nestjs/core": "^11.1.8", - "@nestjs/platform-express": "^11.1.8", - "@nestjs/platform-ws": "^11.1.8", - "@nestjs/testing": "^11.1.8", "@rocketadmin/shared-code": "workspace:*", "@types/pg": "^8.15.6", "argon2": "^0.44.0", + "chalk": "^5.6.2", + "commander": "^14.0.2", "crypto-js": "^4.2.0", + "dotenv": "^17.2.3", "get-port": "^7.1.0", + "inquirer": "^13.0.2", "knex": "3.1.0", "mongodb": "^6.20.0", "mysql2": "^3.15.3", + "ora": "^9.0.0", "oracledb": "^6.10.0", "pg": "^8.16.3", - "readline-sync": "^1.4.10", - "reflect-metadata": "^0.2.2", "rimraf": "^6.0.1", - "rxjs": "^7.8.2", "ssh2": "^1.17.0", "tedious": "^18.6.1", "wait-on": "^9.0.1", "winston": "^3.18.3", + "ws": "^8.18.3", "yarn": "^1.22.22" }, "optionalDependencies": { "ibm_db": "3.3.0" }, "devDependencies": { - "@nestjs/cli": "^11.0.10", - "@nestjs/schematics": "^11.0.9", - "@nestjs/testing": "^11.1.8", - "@types/express": "^5.0.5", + "@types/inquirer": "^9.0.9", "@types/jest": "^30.0.0", "@types/node": "^24.9.1", - "@types/readline-sync": "^1.4.8", "@types/supertest": "^6.0.3", + "@types/ws": "^8.18.1", "@typescript-eslint/eslint-plugin": "^8.46.2", "@typescript-eslint/parser": "^8.46.2", "eslint": "^9.38.0", diff --git a/rocketadmin-agent/src/app.controller.spec.ts b/rocketadmin-agent/src/app.controller.spec.ts deleted file mode 100644 index 82ef017b..00000000 --- a/rocketadmin-agent/src/app.controller.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AppController } from './app.controller.js'; -import { AppService } from './app.service.js'; - -describe('AppController', () => { - let appController: AppController; - - beforeEach(async () => { - const app: TestingModule = await Test.createTestingModule({ - controllers: [AppController], - providers: [AppService], - }).compile(); - - appController = app.get(AppController); - }); - - describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.getHello()).toBe('Hello World!'); - }); - }); -}); diff --git a/rocketadmin-agent/src/app.controller.ts b/rocketadmin-agent/src/app.controller.ts deleted file mode 100644 index 22fbf4d5..00000000 --- a/rocketadmin-agent/src/app.controller.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; -import { AppService } from './app.service.js'; - -@Controller() -export class AppController { - constructor(private readonly appService: AppService) {} - - @Get() - getHello(): string { - return this.appService.getHello(); - } -} diff --git a/rocketadmin-agent/src/app.module.ts b/rocketadmin-agent/src/app.module.ts deleted file mode 100644 index f752e2b0..00000000 --- a/rocketadmin-agent/src/app.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Module } from '@nestjs/common'; -import { AppController } from './app.controller.js'; -import { AppService } from './app.service.js'; -import { ConfigModule } from '@nestjs/config'; - -@Module({ - imports: [ - ConfigModule.forRoot({ - envFilePath: '.config.env', - }), - ], - controllers: [AppController], - providers: [AppService], -}) -export class AppModule {} diff --git a/rocketadmin-agent/src/app.service.ts b/rocketadmin-agent/src/app.service.ts deleted file mode 100644 index 927d7cca..00000000 --- a/rocketadmin-agent/src/app.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class AppService { - getHello(): string { - return 'Hello World!'; - } -} diff --git a/rocketadmin-agent/src/helpers/cli/cli-questions.ts b/rocketadmin-agent/src/helpers/cli/cli-questions.ts deleted file mode 100644 index 4abcb761..00000000 --- a/rocketadmin-agent/src/helpers/cli/cli-questions.ts +++ /dev/null @@ -1,366 +0,0 @@ -import { ConnectionTypesEnum } from '@rocketadmin/shared-code/dist/src/shared/enums/connection-types-enum.js'; -import readlineSync from 'readline-sync'; -import { Messages } from '../../text/messages.js'; -import { Constants } from '../constants/constants.js'; - -export class CLIQuestionUtility { - public static askConnectionToken(): string { - let connectionToken: string = null; - for (let i = 0; i < Constants.CLI_ATTEMPTS_COUNT; i++) { - connectionToken = readlineSync.question(Messages.INTRO_MESSAGES.WELCOME_MESSAGE).trim(); - if (connectionToken === Constants.CLI_QUIT_COMMAND) { - console.log(Messages.INTRO_MESSAGES.APPLICATION_CLI_QUIT); - process.exit(0); - break; - } - if (!connectionToken || connectionToken.length <= 0) { - console.log(Messages.CONNECTION_TOKEN_MISSING); - } else { - return connectionToken; - } - if (i === 2) { - console.log(Messages.INTRO_MESSAGES.APPLICATION_ATTEMPTS_QUIT); - process.exit(0); - } - } - } - - public static askConnectionType(): ConnectionTypesEnum { - console.log(Messages.INTRO_MESSAGES.CONNECTION_TYPE_MESSAGE); - const connectionTypeList: Array = [ - 'PostgreSQL', - 'MySQL', - 'Oracle Database', - 'Microsoft SQL Server', - 'MongoDB', - 'IBM Db2', - 'Redis', - 'ClickHouse', - ]; - const connectionTypeIndex = readlineSync.keyInSelect(connectionTypeList, '-> \n') + 1; - switch (connectionTypeIndex) { - case 1: - console.log(`${connectionTypeList[connectionTypeIndex - 1]} selected.`); - return ConnectionTypesEnum.postgres; - case 2: - console.log(`${connectionTypeList[connectionTypeIndex - 1]} selected.`); - return ConnectionTypesEnum.mysql; - case 3: - console.log(`${connectionTypeList[connectionTypeIndex - 1]} selected.`); - return ConnectionTypesEnum.oracledb; - case 4: - console.log(`${connectionTypeList[connectionTypeIndex - 1]} selected.`); - return ConnectionTypesEnum.mssql; - case 5: - console.log(`${connectionTypeList[connectionTypeIndex - 1]} selected.`); - return ConnectionTypesEnum.mongodb; - case 6: - console.log(`${connectionTypeList[connectionTypeIndex - 1]} selected.`); - return ConnectionTypesEnum.ibmdb2; - case 7: - console.log(`${connectionTypeList[connectionTypeIndex - 1]} selected.`); - return ConnectionTypesEnum.redis; - case 8: - console.log(`${connectionTypeList[connectionTypeIndex - 1]} selected.`); - return ConnectionTypesEnum.clickhouse; - case 0: - console.log(Messages.INTRO_MESSAGES.APPLICATION_CLI_QUIT); - process.exit(0); - break; - default: - console.log(Messages.CONNECTION_TYPE_INVALID); - break; - } - } - - public static askConnectionHost(): string { - let connectionHost: string = null; - for (let i = 0; i < Constants.CLI_ATTEMPTS_COUNT; i++) { - connectionHost = readlineSync.question(Messages.INTRO_MESSAGES.CONNECTION_HOST_MESSAGE).trim(); - if (connectionHost === Constants.CLI_QUIT_COMMAND) { - console.log(Messages.INTRO_MESSAGES.APPLICATION_CLI_QUIT); - process.exit(0); - break; - } - if (!connectionHost || connectionHost.length <= 0) { - console.log(Messages.CONNECTION_HOST_INVALID); - } else { - return connectionHost; - } - if (i === 2) { - console.log(Messages.INTRO_MESSAGES.APPLICATION_ATTEMPTS_QUIT); - } - } - } - - public static askConnectionPort(): number { - let connectionPort: number = null; - for (let i = 0; i < Constants.CLI_ATTEMPTS_COUNT; i++) { - connectionPort = parseInt(readlineSync.question(Messages.INTRO_MESSAGES.CONNECTION_PORT_MESSAGE)); - if (connectionPort < 0 || connectionPort > 65535 || !connectionPort) { - console.log(Messages.CONNECTION_PORT_INVALID); - } else { - return connectionPort; - } - if (i === 2) { - console.log(Messages.INTRO_MESSAGES.APPLICATION_ATTEMPTS_QUIT); - } - } - } - - public static askConnectionUserName(): string { - let connectionUserName: string = null; - for (let i = 0; i < Constants.CLI_ATTEMPTS_COUNT; i++) { - connectionUserName = readlineSync.question(Messages.INTRO_MESSAGES.CONNECTION_USERNAME_MESSAGE).trim(); - if (connectionUserName === Constants.CLI_QUIT_COMMAND) { - console.log(Messages.INTRO_MESSAGES.APPLICATION_CLI_QUIT); - process.exit(0); - break; - } - if (!connectionUserName || connectionUserName.length <= 0) { - console.log(Messages.CONNECTION_USERNAME_INVALID); - } else { - return connectionUserName; - } - if (i === 2) { - console.log(Messages.INTRO_MESSAGES.APPLICATION_ATTEMPTS_QUIT); - } - } - } - - public static askConnectionPassword(message = Messages.INTRO_MESSAGES.CONNECTION_PASSWORD_MESSAGE): string { - let connectionPassword: string = null; - for (let i = 0; i < Constants.CLI_ATTEMPTS_COUNT; i++) { - connectionPassword = readlineSync.question(message).trim(); - if (connectionPassword === Constants.CLI_QUIT_COMMAND) { - console.log(Messages.INTRO_MESSAGES.APPLICATION_CLI_QUIT); - process.exit(0); - break; - } - if (!connectionPassword || connectionPassword.length <= 0) { - console.log(Messages.CONNECTION_PASSWORD_INVALID); - } else { - return connectionPassword; - } - if (i === 2) { - console.log(Messages.INTRO_MESSAGES.APPLICATION_ATTEMPTS_QUIT); - } - } - } - - public static askConnectionDatabase(): string { - let connectionDatabase: string = null; - for (let i = 0; i < Constants.CLI_ATTEMPTS_COUNT; i++) { - connectionDatabase = readlineSync.question(Messages.INTRO_MESSAGES.CONNECTION_DATABASE_MESSAGE).trim(); - if (connectionDatabase === Constants.CLI_QUIT_COMMAND) { - console.log(Messages.INTRO_MESSAGES.APPLICATION_CLI_QUIT); - process.exit(0); - break; - } - if (!connectionDatabase || connectionDatabase.length <= 0) { - console.log(Messages.CONNECTION_DATABASE_INVALID); - } else { - return connectionDatabase; - } - if (i === 2) { - console.log(Messages.INTRO_MESSAGES.APPLICATION_ATTEMPTS_QUIT); - } - } - } - - public static askConnectionSchema(): string { - const connectionSchema = readlineSync.question(Messages.INTRO_MESSAGES.CONNECTION_SCHEMA_MESSAGE).trim(); - if (connectionSchema === Constants.CLI_QUIT_COMMAND) { - console.log(Messages.INTRO_MESSAGES.APPLICATION_CLI_QUIT); - process.exit(0); - } - if (!connectionSchema || connectionSchema.length <= 0) { - return null; - } else { - return connectionSchema; - } - } - - public static askConnectionSid(): string { - const connectionSID = readlineSync.question(Messages.INTRO_MESSAGES.CONNECTION_SID_MESSAGE).trim(); - if (connectionSID === Constants.CLI_QUIT_COMMAND) { - console.log(Messages.INTRO_MESSAGES.APPLICATION_CLI_QUIT); - process.exit(0); - } - if (!connectionSID || connectionSID.length <= 0) { - return null; - } else { - return connectionSID; - } - } - - public static askConnectionAzureEncryption(): boolean { - const yesNoList: Array = ['YES', 'NO']; - for (let i = 0; i < Constants.CLI_ATTEMPTS_COUNT; i++) { - const optionIndex = - readlineSync.keyInSelect(yesNoList, Messages.INTRO_MESSAGES.CONNECTION_AZURE_ENCRYPTION_MESSAGE) + 1; - switch (optionIndex) { - case 1: - console.log(Messages.INTRO_MESSAGES.YOU_CHOOSE(yesNoList[optionIndex - 1])); - return true; - case 2: - console.log(Messages.INTRO_MESSAGES.YOU_CHOOSE(yesNoList[optionIndex - 1])); - return false; - case 0: - console.log(Messages.INTRO_MESSAGES.APPLICATION_CLI_QUIT); - process.exit(0); - break; - default: - break; - } - if (i === 2) { - console.log(Messages.INTRO_MESSAGES.APPLICATION_ATTEMPTS_QUIT); - } - } - } - - public static askConnectionSslOption(): boolean { - const yesNoList: Array = ['YES', 'NO']; - for (let i = 0; i < Constants.CLI_ATTEMPTS_COUNT; i++) { - const optionIndex = - readlineSync.keyInSelect(yesNoList, Messages.INTRO_MESSAGES.CONNECTION_SSL_OPTION_MESSAGE) + 1; - switch (optionIndex) { - case 1: - console.log(Messages.INTRO_MESSAGES.YOU_CHOOSE(yesNoList[optionIndex - 1])); - return true; - case 2: - console.log(Messages.INTRO_MESSAGES.YOU_CHOOSE(yesNoList[optionIndex - 1])); - return false; - case 0: - console.log(Messages.INTRO_MESSAGES.APPLICATION_CLI_QUIT); - process.exit(0); - break; - default: - break; - } - if (i === 2) { - console.log(Messages.INTRO_MESSAGES.APPLICATION_ATTEMPTS_QUIT); - } - } - } - - public static askApplicationSaveConfig(): boolean { - const yesNoList: Array = ['YES', 'NO']; - for (let i = 0; i < Constants.CLI_ATTEMPTS_COUNT; i++) { - const optionIndex = - readlineSync.keyInSelect(yesNoList, Messages.INTRO_MESSAGES.APPLICATION_CONFIG_SAVE_MESSAGE) + 1; - switch (optionIndex) { - case 1: - console.log(Messages.INTRO_MESSAGES.YOU_CHOOSE(yesNoList[optionIndex - 1])); - return true; - case 2: - console.log(Messages.INTRO_MESSAGES.YOU_CHOOSE(yesNoList[optionIndex - 1])); - return false; - case 0: - console.log(Messages.INTRO_MESSAGES.APPLICATION_CLI_QUIT); - process.exit(0); - break; - default: - break; - } - if (i === 2) { - console.log(Messages.INTRO_MESSAGES.APPLICATION_ATTEMPTS_QUIT); - } - } - } - - public static askApplicationEncryptConfigOption(): boolean { - const yesNoList: Array = ['YES', 'NO']; - for (let i = 0; i < Constants.CLI_ATTEMPTS_COUNT; i++) { - const optionIndex = - readlineSync.keyInSelect(yesNoList, Messages.INTRO_MESSAGES.APPLICATION_CONFIG_ENCRYPT_MESSAGE) + 1; - switch (optionIndex) { - case 1: - console.log(Messages.INTRO_MESSAGES.YOU_CHOOSE(yesNoList[optionIndex - 1])); - return true; - case 2: - console.log(Messages.INTRO_MESSAGES.YOU_CHOOSE(yesNoList[optionIndex - 1])); - return false; - case 0: - console.log(Messages.INTRO_MESSAGES.APPLICATION_CLI_QUIT); - process.exit(0); - break; - default: - break; - } - if (i === 2) { - console.log(Messages.INTRO_MESSAGES.APPLICATION_ATTEMPTS_QUIT); - } - } - } - - public static askApplicationEncryptionPassword(): string { - let encPassword: string = null; - for (let i = 0; i < Constants.CLI_ATTEMPTS_COUNT; i++) { - encPassword = readlineSync.question(Messages.INTRO_MESSAGES.APPLICATION_CONFIG_ENCRYPT_PASSWORD_MESSAGE).trim(); - if (encPassword === Constants.CLI_QUIT_COMMAND) { - console.log(Messages.INTRO_MESSAGES.APPLICATION_CLI_QUIT); - process.exit(0); - break; - } - if (!encPassword || encPassword.length <= 0) { - console.log(Messages.APPLICATION_ENCRYPTION_PASSWORD_INVALID); - } else { - return encPassword; - } - if (i === 2) { - console.log(Messages.INTRO_MESSAGES.APPLICATION_ATTEMPTS_QUIT); - } - } - } - - public static askApplicationSaveLogsOption(): boolean { - const yesNoList: Array = ['YES', 'NO']; - for (let i = 0; i < Constants.CLI_ATTEMPTS_COUNT; i++) { - const optionIndex = readlineSync.keyInSelect(yesNoList, Messages.INTRO_MESSAGES.APPLICATION_SAVE_LOGS_OPTION) + 1; - switch (optionIndex) { - case 1: - console.log(Messages.INTRO_MESSAGES.YOU_CHOOSE(yesNoList[optionIndex - 1])); - return true; - case 2: - console.log(Messages.INTRO_MESSAGES.YOU_CHOOSE(yesNoList[optionIndex - 1])); - return false; - case 0: - console.log(Messages.INTRO_MESSAGES.APPLICATION_CLI_QUIT); - process.exit(0); - break; - default: - break; - } - if (i === 2) { - console.log(Messages.INTRO_MESSAGES.APPLICATION_ATTEMPTS_QUIT); - } - } - } - - public static askConnectionDataCenter(): string { - const connectionDataCenter = readlineSync.question(Messages.INTRO_MESSAGES.CONNECTION_DATACENTER_MESSAGE).trim(); - if (connectionDataCenter === Constants.CLI_QUIT_COMMAND) { - console.log(Messages.INTRO_MESSAGES.APPLICATION_CLI_QUIT); - process.exit(0); - } - if (!connectionDataCenter || connectionDataCenter.length <= 0) { - return null; - } else { - return connectionDataCenter; - } - } - - public static askConnectionAuthSource(): string { - const connectionAuthSource = readlineSync.question(Messages.INTRO_MESSAGES.CONNECTION_AUTH_SOURCE_MESSAGE).trim(); - if (connectionAuthSource === Constants.CLI_QUIT_COMMAND) { - console.log(Messages.INTRO_MESSAGES.APPLICATION_CLI_QUIT); - process.exit(0); - } - if (!connectionAuthSource || connectionAuthSource.length <= 0) { - return null; - } else { - return connectionAuthSource; - } - } -} diff --git a/rocketadmin-agent/src/helpers/cli/interactive-prompts.ts b/rocketadmin-agent/src/helpers/cli/interactive-prompts.ts new file mode 100644 index 00000000..e6c6433b --- /dev/null +++ b/rocketadmin-agent/src/helpers/cli/interactive-prompts.ts @@ -0,0 +1,427 @@ +import inquirer from 'inquirer'; +import chalk from 'chalk'; +import { ConnectionTypesEnum } from '@rocketadmin/shared-code/dist/src/shared/enums/connection-types-enum.js'; +import { ICLIConnectionCredentials } from '../../interfaces/interfaces.js'; + +const DATABASE_CHOICES = [ + { name: 'PostgreSQL', value: ConnectionTypesEnum.postgres }, + { name: 'MySQL', value: ConnectionTypesEnum.mysql }, + { name: 'Oracle Database', value: ConnectionTypesEnum.oracledb }, + { name: 'Microsoft SQL Server', value: ConnectionTypesEnum.mssql }, + { name: 'MongoDB', value: ConnectionTypesEnum.mongodb }, + { name: 'IBM Db2', value: ConnectionTypesEnum.ibmdb2 }, + { name: 'Redis', value: ConnectionTypesEnum.redis }, + { name: 'ClickHouse', value: ConnectionTypesEnum.clickhouse }, +]; + +const DEFAULT_PORTS: Record = { + [ConnectionTypesEnum.postgres]: 5432, + [ConnectionTypesEnum.mysql]: 3306, + [ConnectionTypesEnum.oracledb]: 1521, + [ConnectionTypesEnum.mssql]: 1433, + [ConnectionTypesEnum.mongodb]: 27017, + [ConnectionTypesEnum.ibmdb2]: 50000, + [ConnectionTypesEnum.redis]: 6379, + [ConnectionTypesEnum.clickhouse]: 8123, +}; + +export class InteractivePrompts { + static displayWelcome(): void { + console.log(''); + console.log(chalk.cyan('╔════════════════════════════════════════════════════════════════╗')); + console.log( + chalk.cyan('║') + + chalk.white.bold(' RocketAdmin Agent Configuration Wizard ') + + chalk.cyan('║'), + ); + console.log(chalk.cyan('╚════════════════════════════════════════════════════════════════╝')); + console.log(''); + console.log(chalk.gray('This wizard will help you configure your database connection.')); + console.log(chalk.gray('You can quit at any time by pressing Ctrl+C.')); + console.log(''); + } + + static async askConnectionToken(): Promise { + const { token } = await inquirer.prompt([ + { + type: 'input', + name: 'token', + message: chalk.cyan('Enter your connection token from RocketAdmin:'), + validate: (input: string) => { + if (!input || input.trim().length === 0) { + return chalk.red('Connection token is required'); + } + return true; + }, + }, + ]); + return token.trim(); + } + + static async askConnectionType(): Promise { + const { type } = await inquirer.prompt([ + { + type: 'rawlist', + name: 'type', + message: chalk.cyan('Select your database type:'), + choices: DATABASE_CHOICES, + }, + ]); + console.log(chalk.green(`✓ Selected: ${DATABASE_CHOICES.find((c) => c.value === type)?.name}`)); + return type; + } + + static async askConnectionHost(): Promise { + const { host } = await inquirer.prompt([ + { + type: 'input', + name: 'host', + message: chalk.cyan('Enter database host:'), + default: 'localhost', + validate: (input: string) => { + if (!input || input.trim().length === 0) { + return chalk.red('Host is required'); + } + return true; + }, + }, + ]); + return host.trim(); + } + + static async askConnectionPort(dbType: ConnectionTypesEnum): Promise { + // eslint-disable-next-line security/detect-object-injection + const defaultPort = DEFAULT_PORTS[dbType] || 5432; + const { port } = await inquirer.prompt([ + { + type: 'number', + name: 'port', + message: chalk.cyan('Enter database port:'), + default: defaultPort, + validate: (input: number) => { + if (isNaN(input) || input < 1 || input > 65535) { + return chalk.red('Port must be a number between 1 and 65535'); + } + return true; + }, + }, + ]); + return port; + } + + static async askConnectionUserName(): Promise { + const { username } = await inquirer.prompt([ + { + type: 'input', + name: 'username', + message: chalk.cyan('Enter database username:'), + validate: (input: string) => { + if (!input || input.trim().length === 0) { + return chalk.red('Username is required'); + } + return true; + }, + }, + ]); + return username.trim(); + } + + static async askConnectionPassword(message?: string): Promise { + const { password } = await inquirer.prompt([ + { + type: 'password', + name: 'password', + message: message || chalk.cyan('Enter database password:'), + mask: '*', + validate: (input: string) => { + if (!input || input.trim().length === 0) { + return chalk.red('Password is required'); + } + return true; + }, + }, + ]); + return password.trim(); + } + + static async askConnectionDatabase(): Promise { + const { database } = await inquirer.prompt([ + { + type: 'input', + name: 'database', + message: chalk.cyan('Enter database name:'), + validate: (input: string) => { + if (!input || input.trim().length === 0) { + return chalk.red('Database name is required'); + } + return true; + }, + }, + ]); + return database.trim(); + } + + static async askConnectionSchema(): Promise { + const { schema } = await inquirer.prompt([ + { + type: 'input', + name: 'schema', + message: chalk.cyan('Enter schema name (optional, press Enter to skip):'), + }, + ]); + return schema.trim() || null; + } + + static async askConnectionSid(): Promise { + const { sid } = await inquirer.prompt([ + { + type: 'input', + name: 'sid', + message: chalk.cyan('Enter Oracle SID (optional, press Enter to skip):'), + }, + ]); + return sid.trim() || null; + } + + static async askConnectionAzureEncryption(): Promise { + const { azureEncryption } = await inquirer.prompt([ + { + type: 'confirm', + name: 'azureEncryption', + message: chalk.cyan('Enable Azure encryption?'), + default: false, + }, + ]); + return azureEncryption; + } + + static async askConnectionSslOption(): Promise { + const { ssl } = await inquirer.prompt([ + { + type: 'confirm', + name: 'ssl', + message: chalk.cyan('Enable SSL connection?'), + default: false, + }, + ]); + if (ssl) { + console.log( + chalk.yellow('⚠ Make sure to place your SSL certificate as "cert.pem" in the application directory'), + ); + } + return ssl; + } + + static async askConnectionDataCenter(): Promise { + const { dataCenter } = await inquirer.prompt([ + { + type: 'input', + name: 'dataCenter', + message: chalk.cyan('Enter Cassandra data center (optional, press Enter to skip):'), + }, + ]); + return dataCenter.trim() || null; + } + + static async askConnectionAuthSource(): Promise { + const { authSource } = await inquirer.prompt([ + { + type: 'input', + name: 'authSource', + message: chalk.cyan('Enter MongoDB auth source (optional, press Enter to skip):'), + default: 'admin', + }, + ]); + return authSource.trim() || null; + } + + static async askApplicationSaveConfig(): Promise { + const { save } = await inquirer.prompt([ + { + type: 'confirm', + name: 'save', + message: chalk.cyan('Save configuration to file for future use?'), + default: true, + }, + ]); + return save; + } + + static async askApplicationEncryptConfigOption(): Promise { + const { encrypt } = await inquirer.prompt([ + { + type: 'confirm', + name: 'encrypt', + message: chalk.cyan('Encrypt the saved configuration with a password?'), + default: true, + }, + ]); + return encrypt; + } + + static async askApplicationEncryptionPassword(): Promise { + const { password } = await inquirer.prompt([ + { + type: 'password', + name: 'password', + message: chalk.cyan('Enter encryption password:'), + mask: '*', + validate: (input: string) => { + if (!input || input.length < 4) { + return chalk.red('Password must be at least 4 characters'); + } + return true; + }, + }, + ]); + + const { confirmPassword: _confirmPassword } = await inquirer.prompt([ + { + type: 'password', + name: 'confirmPassword', + message: chalk.cyan('Confirm encryption password:'), + mask: '*', + validate: (input: string) => { + // eslint-disable-next-line security/detect-possible-timing-attacks + if (input !== password) { + return chalk.red('Passwords do not match'); + } + return true; + }, + }, + ]); + + return password; + } + + static async askApplicationSaveLogsOption(): Promise { + const { saveLogs } = await inquirer.prompt([ + { + type: 'confirm', + name: 'saveLogs', + message: chalk.cyan('Save application logs to file?'), + default: false, + }, + ]); + return saveLogs; + } + + static async askDecryptionPassword(): Promise { + const { password } = await inquirer.prompt([ + { + type: 'password', + name: 'password', + message: chalk.cyan('Enter your encryption password to decrypt configuration:'), + mask: '*', + validate: (input: string) => { + if (!input || input.length === 0) { + return chalk.red('Password is required'); + } + return true; + }, + }, + ]); + return password; + } + + static async runInteractiveSetup(): Promise { + this.displayWelcome(); + + const credentials: ICLIConnectionCredentials = { + app_port: 3000, + azure_encryption: false, + cert: null, + database: null, + host: null, + password: null, + port: null, + schema: null, + sid: null, + ssl: false, + token: null, + type: null, + username: null, + application_save_option: false, + config_encryption_option: false, + encryption_password: null, + saving_logs_option: false, + dataCenter: null, + authSource: null, + }; + + credentials.token = await this.askConnectionToken(); + credentials.type = await this.askConnectionType(); + credentials.host = await this.askConnectionHost(); + credentials.port = await this.askConnectionPort(credentials.type as ConnectionTypesEnum); + + if (credentials.type !== ConnectionTypesEnum.redis) { + credentials.username = await this.askConnectionUserName(); + } + credentials.password = await this.askConnectionPassword(); + + if (credentials.type !== ConnectionTypesEnum.redis) { + credentials.database = await this.askConnectionDatabase(); + credentials.schema = await this.askConnectionSchema(); + } + + if (credentials.type === ConnectionTypesEnum.oracledb) { + credentials.sid = await this.askConnectionSid(); + } + if (credentials.type === ConnectionTypesEnum.mssql) { + credentials.azure_encryption = await this.askConnectionAzureEncryption(); + } + if (credentials.type === ConnectionTypesEnum.cassandra) { + credentials.dataCenter = await this.askConnectionDataCenter(); + } + if (credentials.type === ConnectionTypesEnum.mongodb) { + credentials.authSource = await this.askConnectionAuthSource(); + } + + credentials.ssl = await this.askConnectionSslOption(); + + console.log(''); + console.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')); + console.log(chalk.white.bold(' Configuration Storage Options')); + console.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')); + console.log(''); + + credentials.application_save_option = await this.askApplicationSaveConfig(); + + if (credentials.application_save_option) { + credentials.config_encryption_option = await this.askApplicationEncryptConfigOption(); + if (credentials.config_encryption_option) { + credentials.encryption_password = await this.askApplicationEncryptionPassword(); + } + } + + credentials.saving_logs_option = await this.askApplicationSaveLogsOption(); + + console.log(''); + console.log(chalk.green('✓ Configuration complete!')); + console.log(''); + + return credentials; + } + + static displayConfigSummary(config: ICLIConnectionCredentials): void { + console.log(''); + console.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')); + console.log(chalk.white.bold(' Connection Configuration')); + console.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')); + console.log(` ${chalk.gray('Database Type:')} ${chalk.white(config.type)}`); + console.log(` ${chalk.gray('Host:')} ${chalk.white(config.host)}`); + console.log(` ${chalk.gray('Port:')} ${chalk.white(config.port)}`); + if (config.username) { + console.log(` ${chalk.gray('Username:')} ${chalk.white('[set]')}`); + } + if (config.database) { + console.log(` ${chalk.gray('Database:')} ${chalk.white(config.database)}`); + } + if (config.schema) { + console.log(` ${chalk.gray('Schema:')} ${chalk.white(config.schema)}`); + } + console.log(` ${chalk.gray('SSL:')} ${chalk.white(config.ssl ? 'Enabled' : 'Disabled')}`); + console.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')); + console.log(''); + } +} diff --git a/rocketadmin-agent/src/main.ts b/rocketadmin-agent/src/main.ts index 7370ed6a..226343c7 100644 --- a/rocketadmin-agent/src/main.ts +++ b/rocketadmin-agent/src/main.ts @@ -1,202 +1,206 @@ -import { NestFactory } from '@nestjs/core'; -import WebSocket from 'ws'; -import { AppModule } from './app.module.js'; -import { CommandExecutor } from './command/command-executor.js'; -import { OperationTypeEnum } from './enums/operation-type.enum.js'; +#!/usr/bin/env node + +import 'dotenv/config'; +import { Command } from 'commander'; +import chalk from 'chalk'; +import ora from 'ora'; +import { ConnectionTypesEnum } from '@rocketadmin/shared-code/dist/src/shared/enums/connection-types-enum.js'; import { checkConnection } from './helpers/check-connection.js'; -import { CLIQuestionUtility } from './helpers/cli/cli-questions.js'; import { Constants } from './helpers/constants/constants.js'; -import { getConnectionToDbParams } from './helpers/get-connection-to-db-params.js'; +import { InteractivePrompts } from './helpers/cli/interactive-prompts.js'; import { mkDirIfNotExistsUtil } from './helpers/write-file-util.js'; import { ICLIConnectionCredentials } from './interfaces/interfaces.js'; import { Config } from './shared/config/config.js'; -import { Messages } from './text/messages.js'; -import { ConnectionTypesEnum } from '@rocketadmin/shared-code/dist/src/shared/enums/connection-types-enum.js'; +import { createWebSocketClient } from './websocket/websocket-client.js'; +import { readFileSync } from 'fs'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +let version = '0.0.1'; +try { + const packageJson = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8')); + version = packageJson.version; +} catch {} + +const program = new Command(); + +program + .name('rocketadmin-agent') + .description('RocketAdmin Agent - Connect your database to RocketAdmin through a secure agent') + .version(version) + .option('-t, --token ', 'Connection token from RocketAdmin') + .option('--type ', 'Database type (postgres, mysql, oracledb, mssql, mongodb, ibmdb2, redis, clickhouse)') + .option('-H, --host ', 'Database host') + .option('-p, --port ', 'Database port', parseInt) + .option('-u, --username ', 'Database username') + .option('-P, --password ', 'Database password') + .option('-d, --database ', 'Database name') + .option('-s, --schema ', 'Database schema') + .option('--sid ', 'Oracle SID') + .option('--ssl', 'Enable SSL connection', false) + .option('--azure-encryption', 'Enable Azure encryption (MSSQL)', false) + .option('--data-center ', 'Cassandra data center') + .option('--auth-source ', 'MongoDB auth source') + .option('--ws-url ', 'WebSocket server URL (overrides REMOTE_WEBSOCKET_ADDRESS env)') + .option('--save-config', 'Save configuration to file', false) + .option('--encrypt-config', 'Encrypt saved configuration', false) + .option('--encryption-password ', 'Password for config encryption') + .option('--save-logs', 'Save logs to file', false) + .option('-i, --interactive', 'Run interactive configuration wizard', false); -async function bootstrap() { - const connectionCredentials: ICLIConnectionCredentials = Config.getConnectionConfig(); - await NestFactory.create(AppModule); - const remoteWebsocketAddRess = process.env.REMOTE_WEBSOCKET_ADDRESS || 'wss://ws.rocketadmin.com:443/'; - function connect() { - const ws = new WebSocket(remoteWebsocketAddRess); - - ws.on('open', function open() { - const connectionToken = connectionCredentials?.token; - if (!connectionToken) { - console.error(Messages.CONNECTION_TOKEN_MISSING); - process.exit(0); - } - console.info('-> Connected to the remote server'); - const data = { - operationType: 'initialConnection', - connectionToken: connectionToken, - }; - ws.send(JSON.stringify(data)); - }); - - ws.on('message', async function incoming(data: any) { - const messageData = JSON.parse(data); - const { - data: { resId }, - } = messageData; - const commandExecutor = new CommandExecutor(connection); - try { - const result = await commandExecutor.executeCommand(messageData); - const responseData = { - operationType: OperationTypeEnum.dataFromAgent, - commandResult: result, - resId: resId, - }; - ws.send(JSON.stringify(responseData)); - } catch (e) { - ws.send(JSON.stringify(e)); - } - }); - - ws.on('close', (code, reason) => { - console.log( - `${Messages.SOCKET_WAS_DISCONNECTED} ${code ? ` With code: ${code} ` : ' '}`, - reason ? `Reason: ${reason}` : '', - ); - setTimeout(() => { - connect(); - }, 1000); - }); - - ws.on('error', (e) => { - console.error(Messages.SOCKET_ENCOUNTERED_ERROR(e.message)); - ws.close(); - }); +async function testDatabaseConnection(connection: ICLIConnectionCredentials, maxRetries: number = 6): Promise { + const spinner = ora({ + text: 'Testing database connection...', + color: 'cyan', + }).start(); + + const testResult = await checkConnection(connection); + if (testResult.result) { + spinner.succeed(chalk.green('Database connection successful')); + return true; } - console.log('-> Application started'); - const connection = await getConnectionToDbParams(); + spinner.warn(chalk.yellow('Initial connection failed, retrying...')); - async function tryConnectToDatabase(timeout = 2000) { - const testConnectionResult = await checkConnection(connection); - if (testConnectionResult.result) { - return; + for (let attempt = 1; attempt <= maxRetries; attempt++) { + const delay = 2000 + attempt * 2000; + console.log(chalk.gray(` Retry ${attempt}/${maxRetries} in ${delay / 1000}s...`)); + + await new Promise((resolve) => setTimeout(resolve, delay)); + + const retryResult = await checkConnection(connection); + if (retryResult.result) { + console.log(chalk.green(`✓ Database connection successful on retry ${attempt}`)); + return true; } - let counter = 0; - setTimeout(async function run() { - timeout += 2000; - ++counter; - const tryResult = await checkConnection(connection); - if (tryResult.result) { - return; - } else { - if (counter >= 6) { - console.log('-> Connection to database failed. Please check your credentials and network connection'); - process.exit(0); - return; - } - setTimeout(run, timeout); - } - }, timeout); } - await tryConnectToDatabase(); - - connect(); + console.error(chalk.red('✗ Database connection failed after all retries')); + console.error(chalk.red(' Please check your credentials and network connection')); + return false; } -(async function () { - const connectionCredentials: ICLIConnectionCredentials = { - app_port: 3000, - azure_encryption: false, +function buildCredentialsFromOptions(options: Record): ICLIConnectionCredentials | null { + if (!options.token || !options.type || !options.host || !options.port) { + return null; + } + + const validTypes = Object.values(ConnectionTypesEnum); + if (!validTypes.includes(options.type as ConnectionTypesEnum)) { + console.error(chalk.red(`Invalid database type: ${options.type}`)); + console.error(chalk.gray(`Valid types: ${validTypes.join(', ')}`)); + process.exit(1); + } + + return { + token: options.token, + type: options.type, + host: options.host, + port: options.port, + username: options.username || null, + password: options.password || null, + database: options.database || null, + schema: options.schema || null, + sid: options.sid || null, + ssl: options.ssl || false, cert: null, - database: null, - host: null, - password: null, - port: null, - schema: null, - sid: null, - ssl: false, - token: null, - type: null, - username: null, - application_save_option: false, - config_encryption_option: false, - encryption_password: null, - saving_logs_option: false, - dataCenter: null, - authSource: null, + app_port: 3000, + azure_encryption: options.azureEncryption || false, + application_save_option: options.saveConfig || false, + config_encryption_option: options.encryptConfig || false, + encryption_password: options.encryptionPassword || null, + saving_logs_option: options.saveLogs || false, + dataCenter: options.dataCenter || null, + authSource: options.authSource || null, }; +} + +async function startAgent(config: ICLIConnectionCredentials, wsUrl?: string): Promise { + console.log(''); + console.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')); + console.log(chalk.white.bold(' RocketAdmin Agent Starting')); + console.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')); + console.log(''); + + InteractivePrompts.displayConfigSummary(config); + + const connected = await testDatabaseConnection(config); + if (!connected) { + process.exit(1); + } - const configFromEnvironment = Config.readConnectionConfigFromEnv(); - if (configFromEnvironment) { - await Config.setConnectionConfig(configFromEnvironment); - bootstrap() - .then(() => { - console.info('-> Application launched'); - }) - .catch((e) => { - console.error(`-> Failed to start application with error: ${e}`); - process.exit(0); - }); - return; - } else { - const savedConfig: ICLIConnectionCredentials = await Config.readConfigFromFile(); - if (savedConfig) { + if (config.saving_logs_option) { + await mkDirIfNotExistsUtil(Constants.DEFAULT_LOGS_DIRNAME); + console.log(chalk.gray('→ Logs will be saved to', Constants.DEFAULT_LOGS_DIRNAME)); + } + + const client = createWebSocketClient(config, wsUrl); + client.connect(); + + console.log(''); + console.log(chalk.green('Agent is running. Press Ctrl+C to stop.')); +} + +function getWebSocketUrl(cliOption?: string): string { + const defaultUrl = 'wss://ws.rocketadmin.com:443/'; + const wsUrl = cliOption || process.env.REMOTE_WEBSOCKET_ADDRESS || defaultUrl; + console.log(chalk.gray(`→ WebSocket URL: ${wsUrl}`)); + console.log(chalk.gray(` (env REMOTE_WEBSOCKET_ADDRESS: ${process.env.REMOTE_WEBSOCKET_ADDRESS || 'not set'})`)); + return wsUrl; +} + +async function main(): Promise { + program.parse(); + const options = program.opts(); + const wsUrl = getWebSocketUrl(options.wsUrl); + + try { + const envConfig = Config.readConnectionConfigFromEnv(); + if (envConfig) { + console.log(chalk.green('→ Configuration loaded from environment variables')); + await Config.setConnectionConfig(envConfig); + await startAgent(envConfig, wsUrl); + return; + } + + const cliConfig = buildCredentialsFromOptions(options); + if (cliConfig && !options.interactive) { + console.log(chalk.green('→ Configuration loaded from command line arguments')); + await Config.setConnectionConfig(cliConfig, cliConfig.application_save_option); + await startAgent(cliConfig, wsUrl); + return; + } + + const savedConfig = await Config.readConfigFromFile(); + if (savedConfig && !options.interactive) { + console.log(chalk.green('→ Configuration loaded from saved file')); await Config.setConnectionConfig(savedConfig); - bootstrap() - .then(() => { - console.info('-> Application launched'); - }) - .catch((e) => { - console.error(`-> Failed to start application with error: ${e}`); - process.exit(0); - }); - } else { - connectionCredentials.token = CLIQuestionUtility.askConnectionToken(); - connectionCredentials.type = CLIQuestionUtility.askConnectionType(); - connectionCredentials.host = CLIQuestionUtility.askConnectionHost(); - connectionCredentials.port = CLIQuestionUtility.askConnectionPort(); - if (connectionCredentials.type !== ConnectionTypesEnum.redis) { - connectionCredentials.username = CLIQuestionUtility.askConnectionUserName(); - } - connectionCredentials.password = CLIQuestionUtility.askConnectionPassword(); - if (connectionCredentials.type !== ConnectionTypesEnum.redis) { - connectionCredentials.database = CLIQuestionUtility.askConnectionDatabase(); - connectionCredentials.schema = CLIQuestionUtility.askConnectionSchema(); - } - if (connectionCredentials.type === ConnectionTypesEnum.oracledb) { - connectionCredentials.sid = CLIQuestionUtility.askConnectionSid(); - } - if (connectionCredentials.type === ConnectionTypesEnum.mssql) { - connectionCredentials.azure_encryption = CLIQuestionUtility.askConnectionAzureEncryption(); - } - if (connectionCredentials.type === ConnectionTypesEnum.cassandra) { - connectionCredentials.dataCenter = CLIQuestionUtility.askConnectionDataCenter(); - } - if (connectionCredentials.type === ConnectionTypesEnum.mongodb) { - connectionCredentials.authSource = CLIQuestionUtility.askConnectionAuthSource(); - } - connectionCredentials.cert = null; - connectionCredentials.ssl = CLIQuestionUtility.askConnectionSslOption(); - connectionCredentials.application_save_option = CLIQuestionUtility.askApplicationSaveConfig(); - if (connectionCredentials.application_save_option) { - connectionCredentials.config_encryption_option = CLIQuestionUtility.askApplicationEncryptConfigOption(); - } - if (connectionCredentials.config_encryption_option) { - connectionCredentials.encryption_password = CLIQuestionUtility.askApplicationEncryptionPassword(); - } - connectionCredentials.saving_logs_option = CLIQuestionUtility.askApplicationSaveLogsOption(); - if (connectionCredentials.saving_logs_option) { - await mkDirIfNotExistsUtil(Constants.DEFAULT_LOGS_DIRNAME); - } - console.info(Messages.CREDENTIALS_ACCEPTED); - - await Config.setConnectionConfig(connectionCredentials, true); - - bootstrap() - .then(() => { - console.info('-> Application launched'); - }) - .catch((e) => { - console.error(`-> Failed to start apllication with error: ${e}`); - process.exit(0); - }); + await startAgent(savedConfig, wsUrl); + return; } + + console.log(chalk.gray('No existing configuration found. Starting interactive setup...')); + const interactiveConfig = await InteractivePrompts.runInteractiveSetup(); + + if (interactiveConfig.saving_logs_option) { + await mkDirIfNotExistsUtil(Constants.DEFAULT_LOGS_DIRNAME); + } + + await Config.setConnectionConfig(interactiveConfig, true); + await startAgent(interactiveConfig, wsUrl); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ERR_USE_AFTER_CLOSE') { + console.log(chalk.yellow('\nSetup cancelled.')); + process.exit(0); + } + console.error(chalk.red('Failed to start agent:'), error); + process.exit(1); } -})(); +} + +main().catch((error) => { + console.error(chalk.red('Unexpected error:'), error); + process.exit(1); +}); diff --git a/rocketadmin-agent/src/shared/config/config.ts b/rocketadmin-agent/src/shared/config/config.ts index d81fdeff..1a83fb00 100644 --- a/rocketadmin-agent/src/shared/config/config.ts +++ b/rocketadmin-agent/src/shared/config/config.ts @@ -5,7 +5,7 @@ import { writeFileIfNotExistsUtil } from '../../helpers/write-file-util.js'; import { validateConnectionData } from '../../helpers/validate-connection-data.js'; import { toPrettyErrorsMsg } from '../../helpers/to-pretty-errors-msg.js'; import { Encryptor } from '../../helpers/encryption/encryptor.js'; -import { CLIQuestionUtility } from '../../helpers/cli/cli-questions.js'; +import { InteractivePrompts } from '../../helpers/cli/interactive-prompts.js'; import { Constants } from '../../helpers/constants/constants.js'; import { Messages } from '../../text/messages.js'; @@ -115,9 +115,7 @@ export class Config { if (appConfig.encrypted) { let encryptionPassword = null; for (let i = 0; i < Constants.CLI_ATTEMPTS_COUNT; i++) { - encryptionPassword = CLIQuestionUtility.askConnectionPassword( - Messages.INTRO_MESSAGES.ASK_ENCRYPTION_PASSWORD_MESSAGE, - ); + encryptionPassword = await InteractivePrompts.askDecryptionPassword(); if (!(await Encryptor.verifyPassword(appConfig.hash, encryptionPassword))) { console.log(Messages.CORRUPTED_DATA_OR_PASSWORD); if (i === 2) { diff --git a/rocketadmin-agent/src/websocket/websocket-client.ts b/rocketadmin-agent/src/websocket/websocket-client.ts new file mode 100644 index 00000000..7cb0d245 --- /dev/null +++ b/rocketadmin-agent/src/websocket/websocket-client.ts @@ -0,0 +1,176 @@ +import WebSocket from 'ws'; +import chalk from 'chalk'; +import ora from 'ora'; +import { CommandExecutor } from '../command/command-executor.js'; +import { OperationTypeEnum } from '../enums/operation-type.enum.js'; +import { ICLIConnectionCredentials } from '../interfaces/interfaces.js'; + +export interface WebSocketClientOptions { + serverUrl: string; + connectionToken: string; + connectionConfig: ICLIConnectionCredentials; + reconnectInterval?: number; + maxReconnectAttempts?: number; +} + +export class WebSocketClient { + private ws: WebSocket | null = null; + private readonly serverUrl: string; + private readonly connectionToken: string; + private readonly connectionConfig: ICLIConnectionCredentials; + private readonly reconnectInterval: number; + private readonly maxReconnectAttempts: number; + private reconnectAttempts: number = 0; + private isShuttingDown: boolean = false; + private spinner: ReturnType | null = null; + + constructor(options: WebSocketClientOptions) { + this.serverUrl = options.serverUrl; + this.connectionToken = options.connectionToken; + this.connectionConfig = options.connectionConfig; + this.reconnectInterval = options.reconnectInterval || 1000; + this.maxReconnectAttempts = options.maxReconnectAttempts || Infinity; + } + + public connect(): void { + if (this.isShuttingDown) { + return; + } + + this.spinner = ora({ + text: 'Connecting to RocketAdmin server...', + color: 'cyan', + }).start(); + + this.ws = new WebSocket(this.serverUrl); + + this.ws.on('open', this.handleOpen.bind(this)); + this.ws.on('message', this.handleMessage.bind(this)); + this.ws.on('close', this.handleClose.bind(this)); + this.ws.on('error', this.handleError.bind(this)); + } + + public disconnect(): void { + this.isShuttingDown = true; + if (this.ws) { + this.ws.close(); + this.ws = null; + } + } + + private handleOpen(): void { + this.reconnectAttempts = 0; + + if (this.spinner) { + this.spinner.succeed(chalk.green('Connected to RocketAdmin server')); + } + + const data = { + operationType: 'initialConnection', + connectionToken: this.connectionToken, + }; + this.ws?.send(JSON.stringify(data)); + console.log(chalk.gray('→ Waiting for commands...')); + } + + private async handleMessage(data: WebSocket.RawData): Promise { + try { + const messageData = JSON.parse(data.toString()); + const { + data: { resId }, + } = messageData; + + const commandExecutor = new CommandExecutor(this.connectionConfig); + + try { + const result = await commandExecutor.executeCommand(messageData); + const responseData = { + operationType: OperationTypeEnum.dataFromAgent, + commandResult: result, + resId: resId, + }; + this.ws?.send(JSON.stringify(responseData)); + } catch (e) { + this.ws?.send(JSON.stringify(e)); + } + } catch (e) { + console.error(chalk.red('Failed to process message:'), e); + } + } + + private handleClose(code: number, reason: Buffer): void { + if (this.isShuttingDown) { + console.log(chalk.yellow('Disconnected from server')); + return; + } + + const reasonStr = reason.toString(); + console.log(chalk.yellow(`Connection closed${code ? ` (code: ${code})` : ''}${reasonStr ? `: ${reasonStr}` : ''}`)); + + this.scheduleReconnect(); + } + + private handleError(error: Error): void { + if (this.spinner) { + this.spinner.fail(chalk.red(`Connection error: ${error.message}`)); + } else { + console.error(chalk.red(`WebSocket error: ${error.message}`)); + } + + if (this.ws) { + this.ws.close(); + } + } + + private scheduleReconnect(): void { + if (this.isShuttingDown) { + return; + } + + if (this.reconnectAttempts >= this.maxReconnectAttempts) { + console.error(chalk.red('Maximum reconnection attempts reached. Exiting...')); + process.exit(1); + } + + this.reconnectAttempts++; + const delay = Math.min(this.reconnectInterval * this.reconnectAttempts, 30000); // Max 30 seconds + + console.log(chalk.gray(`Reconnecting in ${delay / 1000}s... (attempt ${this.reconnectAttempts})`)); + + setTimeout(() => { + this.connect(); + }, delay); + } +} + +export function createWebSocketClient( + connectionConfig: ICLIConnectionCredentials, + serverUrl?: string, +): WebSocketClient { + const wsUrl = serverUrl || process.env.REMOTE_WEBSOCKET_ADDRESS || 'wss://ws.rocketadmin.com:443/'; + + if (!connectionConfig.token) { + console.error(chalk.red('Connection token is missing')); + process.exit(1); + } + + const client = new WebSocketClient({ + serverUrl: wsUrl, + connectionToken: connectionConfig.token, + connectionConfig: connectionConfig, + }); + + process.on('SIGINT', () => { + console.log(chalk.yellow('\nReceived SIGINT. Gracefully shutting down...')); + client.disconnect(); + process.exit(0); + }); + + process.on('SIGTERM', () => { + console.log(chalk.yellow('\nReceived SIGTERM. Gracefully shutting down...')); + client.disconnect(); + process.exit(0); + }); + + return client; +} diff --git a/yarn.lock b/yarn.lock index 8049da38..72ca52f3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1671,6 +1671,13 @@ __metadata: languageName: node linkType: hard +"@inquirer/ansi@npm:^2.0.2": + version: 2.0.2 + resolution: "@inquirer/ansi@npm:2.0.2" + checksum: 2755a5547c539734ac8eccaf475533a8c4c1b39c4dd5f9d7926030f824284ea750a59c9e0366be5cdee771885c32a0de65d7c548253615cd7058142e0f6a106a + languageName: node + linkType: hard + "@inquirer/checkbox@npm:^4.1.2, @inquirer/checkbox@npm:^4.2.0": version: 4.3.0 resolution: "@inquirer/checkbox@npm:4.3.0" @@ -1689,6 +1696,23 @@ __metadata: languageName: node linkType: hard +"@inquirer/checkbox@npm:^5.0.2": + version: 5.0.2 + resolution: "@inquirer/checkbox@npm:5.0.2" + dependencies: + "@inquirer/ansi": ^2.0.2 + "@inquirer/core": ^11.0.2 + "@inquirer/figures": ^2.0.2 + "@inquirer/type": ^4.0.2 + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 7d0aac9bf086ab7b114908ae9bae4d45de211af3547d8caca564606422bbc15c17936e8526c9244d03ba10ffda5f9706707c3a15a17cb916188cd93d03efda1a + languageName: node + linkType: hard + "@inquirer/confirm@npm:^5.1.14, @inquirer/confirm@npm:^5.1.6": version: 5.1.19 resolution: "@inquirer/confirm@npm:5.1.19" @@ -1704,6 +1728,21 @@ __metadata: languageName: node linkType: hard +"@inquirer/confirm@npm:^6.0.2": + version: 6.0.2 + resolution: "@inquirer/confirm@npm:6.0.2" + dependencies: + "@inquirer/core": ^11.0.2 + "@inquirer/type": ^4.0.2 + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 8e3aa49cfa02e44d3de73c00e95f99d59cf95ad3eceb4098a46edf764ef73bbcf9becdc2384b8d7ac24fbeffa99f350b934bf9b2954401d280460ba4cb28dcef + languageName: node + linkType: hard + "@inquirer/core@npm:^10.3.0": version: 10.3.0 resolution: "@inquirer/core@npm:10.3.0" @@ -1725,6 +1764,26 @@ __metadata: languageName: node linkType: hard +"@inquirer/core@npm:^11.0.2": + version: 11.0.2 + resolution: "@inquirer/core@npm:11.0.2" + dependencies: + "@inquirer/ansi": ^2.0.2 + "@inquirer/figures": ^2.0.2 + "@inquirer/type": ^4.0.2 + cli-width: ^4.1.0 + mute-stream: ^3.0.0 + signal-exit: ^4.1.0 + wrap-ansi: ^9.0.2 + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 14bac6f6c23fffabab48f20bc45d37033e663ba09f1ce8103355de3b77aedf57a85b4a9313ca32257b91f9bb0100bb5566fdace19af1b7e789d4104b27a2ab7f + languageName: node + linkType: hard + "@inquirer/editor@npm:^4.2.15, @inquirer/editor@npm:^4.2.7": version: 4.2.21 resolution: "@inquirer/editor@npm:4.2.21" @@ -1741,6 +1800,22 @@ __metadata: languageName: node linkType: hard +"@inquirer/editor@npm:^5.0.2": + version: 5.0.2 + resolution: "@inquirer/editor@npm:5.0.2" + dependencies: + "@inquirer/core": ^11.0.2 + "@inquirer/external-editor": ^2.0.2 + "@inquirer/type": ^4.0.2 + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 8217a81f045091b525bbfeb4ccb2bc361962fea993134773a05ede05ca57b466a8b1c6e6d0dc09810248b1a80ecc344cc4602c786ee8e67dc097429daa1ba3a3 + languageName: node + linkType: hard + "@inquirer/expand@npm:^4.0.17, @inquirer/expand@npm:^4.0.9": version: 4.0.21 resolution: "@inquirer/expand@npm:4.0.21" @@ -1757,6 +1832,21 @@ __metadata: languageName: node linkType: hard +"@inquirer/expand@npm:^5.0.2": + version: 5.0.2 + resolution: "@inquirer/expand@npm:5.0.2" + dependencies: + "@inquirer/core": ^11.0.2 + "@inquirer/type": ^4.0.2 + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 7cc47c52eb92c92c3735e1ec898baa437d175f46741cbdf075032f9be6fb0e25f431f029b499efad720772d591b6e5f111756a9a4c93ae5e928c362d3ea44758 + languageName: node + linkType: hard + "@inquirer/external-editor@npm:^1.0.2": version: 1.0.2 resolution: "@inquirer/external-editor@npm:1.0.2" @@ -1772,6 +1862,21 @@ __metadata: languageName: node linkType: hard +"@inquirer/external-editor@npm:^2.0.2": + version: 2.0.2 + resolution: "@inquirer/external-editor@npm:2.0.2" + dependencies: + chardet: ^2.1.1 + iconv-lite: ^0.7.0 + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 6c3ec527da6e15d811a44102a163661cdc4405bdfed416172e0e40f618e979b4df4a426a8bb95b5961bbbb41aa72bad0b908835490e3be88390a553d0f9a15b1 + languageName: node + linkType: hard + "@inquirer/figures@npm:^1.0.14": version: 1.0.14 resolution: "@inquirer/figures@npm:1.0.14" @@ -1779,6 +1884,13 @@ __metadata: languageName: node linkType: hard +"@inquirer/figures@npm:^2.0.2": + version: 2.0.2 + resolution: "@inquirer/figures@npm:2.0.2" + checksum: 6a573f41c9077ccf0205840c1b79cf595e82095731c3badf80e738f5240928e80c0e2e05a1782794ddefe00950d4b6b5018c7bc37e74de69c1c23b7a886e6563 + languageName: node + linkType: hard + "@inquirer/input@npm:^4.1.6, @inquirer/input@npm:^4.2.1": version: 4.2.5 resolution: "@inquirer/input@npm:4.2.5" @@ -1794,6 +1906,21 @@ __metadata: languageName: node linkType: hard +"@inquirer/input@npm:^5.0.2": + version: 5.0.2 + resolution: "@inquirer/input@npm:5.0.2" + dependencies: + "@inquirer/core": ^11.0.2 + "@inquirer/type": ^4.0.2 + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 010251e6be2ce91db1052a3dbd575a6b5173b35b87358728abf833c448eba9d0fe9786fec93da5f805d1cac5dbd59d76c7e238214c35ad2307f613f2d357e21a + languageName: node + linkType: hard + "@inquirer/number@npm:^3.0.17, @inquirer/number@npm:^3.0.9": version: 3.0.21 resolution: "@inquirer/number@npm:3.0.21" @@ -1809,6 +1936,21 @@ __metadata: languageName: node linkType: hard +"@inquirer/number@npm:^4.0.2": + version: 4.0.2 + resolution: "@inquirer/number@npm:4.0.2" + dependencies: + "@inquirer/core": ^11.0.2 + "@inquirer/type": ^4.0.2 + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: ca1e1f6233b3d598cba9349cb20127907c131a8606a6fce894b2ee1818f633c80354c303e28aacbad405c275425b0b86b60856eacca2101e0af00b569db6633f + languageName: node + linkType: hard + "@inquirer/password@npm:^4.0.17, @inquirer/password@npm:^4.0.9": version: 4.0.21 resolution: "@inquirer/password@npm:4.0.21" @@ -1825,6 +1967,22 @@ __metadata: languageName: node linkType: hard +"@inquirer/password@npm:^5.0.2": + version: 5.0.2 + resolution: "@inquirer/password@npm:5.0.2" + dependencies: + "@inquirer/ansi": ^2.0.2 + "@inquirer/core": ^11.0.2 + "@inquirer/type": ^4.0.2 + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: e162bb95d78dcee458f7da5a446da4b072ad310deb618cddeb95e29aed9ba11073079ed1733f4665d50dd6f8264e405b3b55187792a09bd7b25549daf352c963 + languageName: node + linkType: hard + "@inquirer/prompts@npm:7.3.2": version: 7.3.2 resolution: "@inquirer/prompts@npm:7.3.2" @@ -1871,6 +2029,29 @@ __metadata: languageName: node linkType: hard +"@inquirer/prompts@npm:^8.0.2": + version: 8.0.2 + resolution: "@inquirer/prompts@npm:8.0.2" + dependencies: + "@inquirer/checkbox": ^5.0.2 + "@inquirer/confirm": ^6.0.2 + "@inquirer/editor": ^5.0.2 + "@inquirer/expand": ^5.0.2 + "@inquirer/input": ^5.0.2 + "@inquirer/number": ^4.0.2 + "@inquirer/password": ^5.0.2 + "@inquirer/rawlist": ^5.0.2 + "@inquirer/search": ^4.0.2 + "@inquirer/select": ^5.0.2 + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 7a917444f8af5063735bd6ee55d65044bd22bcba81e03140df6f76f11d42d16bb5dc8dc91ce3f0a273d27965760c2e3ac65e18cdf6822c9e95cd705bd4b87e8c + languageName: node + linkType: hard + "@inquirer/rawlist@npm:^4.0.9, @inquirer/rawlist@npm:^4.1.5": version: 4.1.9 resolution: "@inquirer/rawlist@npm:4.1.9" @@ -1887,6 +2068,21 @@ __metadata: languageName: node linkType: hard +"@inquirer/rawlist@npm:^5.0.2": + version: 5.0.2 + resolution: "@inquirer/rawlist@npm:5.0.2" + dependencies: + "@inquirer/core": ^11.0.2 + "@inquirer/type": ^4.0.2 + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 423eaadb4625b1c5a4ca4144abf3ed2bfd2908bd62a826f7f4663841ff57dc2fd90b4fd028b9aaf030fd5d746ebdeffc4fad73300922f8dbee6af3f25f82e840 + languageName: node + linkType: hard + "@inquirer/search@npm:^3.0.9, @inquirer/search@npm:^3.1.0": version: 3.2.0 resolution: "@inquirer/search@npm:3.2.0" @@ -1904,6 +2100,22 @@ __metadata: languageName: node linkType: hard +"@inquirer/search@npm:^4.0.2": + version: 4.0.2 + resolution: "@inquirer/search@npm:4.0.2" + dependencies: + "@inquirer/core": ^11.0.2 + "@inquirer/figures": ^2.0.2 + "@inquirer/type": ^4.0.2 + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: ccaa8f24737a6e37fb6166aa6cf05e5dc2f0d73e15b1938463d7e78a242983ae0916f2b224575545ab218c7298b82548e9329ae2d0a32a3a38fa08dce58d3b85 + languageName: node + linkType: hard + "@inquirer/select@npm:^4.0.9, @inquirer/select@npm:^4.3.1": version: 4.4.0 resolution: "@inquirer/select@npm:4.4.0" @@ -1922,6 +2134,23 @@ __metadata: languageName: node linkType: hard +"@inquirer/select@npm:^5.0.2": + version: 5.0.2 + resolution: "@inquirer/select@npm:5.0.2" + dependencies: + "@inquirer/ansi": ^2.0.2 + "@inquirer/core": ^11.0.2 + "@inquirer/figures": ^2.0.2 + "@inquirer/type": ^4.0.2 + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 85f93b22a69c9623ba0ceac71a26e45f4405cc429e4d4d75f1b5e24bd871b6ac7deeae5a3f808a82548fe12a7a9ca81c43962bfeed28ef78451a344b625f848d + languageName: node + linkType: hard + "@inquirer/type@npm:^3.0.9": version: 3.0.9 resolution: "@inquirer/type@npm:3.0.9" @@ -1934,6 +2163,18 @@ __metadata: languageName: node linkType: hard +"@inquirer/type@npm:^4.0.2": + version: 4.0.2 + resolution: "@inquirer/type@npm:4.0.2" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 1611281b5a6c9b6af6d4c8aee58bf893f9b059337c1300d5ec547dd33342756e56b17220106851fbf04cafd8a2549301bc9478a770209f79dbdad04e6de8d733 + languageName: node + linkType: hard + "@isaacs/balanced-match@npm:^4.0.1": version: 4.0.1 resolution: "@isaacs/balanced-match@npm:4.0.1" @@ -2443,7 +2684,7 @@ __metadata: languageName: node linkType: hard -"@nestjs/common@npm:11.1.8, @nestjs/common@npm:^11.1.8": +"@nestjs/common@npm:11.1.8": version: 11.1.8 resolution: "@nestjs/common@npm:11.1.8" dependencies: @@ -2466,7 +2707,7 @@ __metadata: languageName: node linkType: hard -"@nestjs/config@npm:4.0.2, @nestjs/config@npm:^4.0.2": +"@nestjs/config@npm:4.0.2": version: 4.0.2 resolution: "@nestjs/config@npm:4.0.2" dependencies: @@ -2480,7 +2721,7 @@ __metadata: languageName: node linkType: hard -"@nestjs/core@npm:11.1.8, @nestjs/core@npm:^11.1.8": +"@nestjs/core@npm:11.1.8": version: 11.1.8 resolution: "@nestjs/core@npm:11.1.8" dependencies: @@ -2525,7 +2766,7 @@ __metadata: languageName: node linkType: hard -"@nestjs/platform-express@npm:11.1.8, @nestjs/platform-express@npm:^11.1.8": +"@nestjs/platform-express@npm:11.1.8": version: 11.1.8 resolution: "@nestjs/platform-express@npm:11.1.8" dependencies: @@ -2541,20 +2782,6 @@ __metadata: languageName: node linkType: hard -"@nestjs/platform-ws@npm:^11.1.8": - version: 11.1.8 - resolution: "@nestjs/platform-ws@npm:11.1.8" - dependencies: - tslib: 2.8.1 - ws: 8.18.3 - peerDependencies: - "@nestjs/common": ^11.0.0 - "@nestjs/websockets": ^11.0.0 - rxjs: ^7.1.0 - checksum: 238384f801b7777377cdb7ade4f2e0af846f812b8e55e973b082c26c9e1ff6b00107589f9ecdfb447ff96b5e7da7a3184c57b9a94dcf00562f2333e149010e2b - languageName: node - linkType: hard - "@nestjs/schedule@npm:^6.0.1": version: 6.0.1 resolution: "@nestjs/schedule@npm:6.0.1" @@ -2567,7 +2794,7 @@ __metadata: languageName: node linkType: hard -"@nestjs/schematics@npm:11.0.9, @nestjs/schematics@npm:^11.0.1, @nestjs/schematics@npm:^11.0.9": +"@nestjs/schematics@npm:11.0.9, @nestjs/schematics@npm:^11.0.1": version: 11.0.9 resolution: "@nestjs/schematics@npm:11.0.9" dependencies: @@ -4486,6 +4713,16 @@ __metadata: languageName: node linkType: hard +"@types/inquirer@npm:^9.0.9": + version: 9.0.9 + resolution: "@types/inquirer@npm:9.0.9" + dependencies: + "@types/through": "*" + rxjs: ^7.2.0 + checksum: 2bbd08c04a554d28289b4b31631aacca552e80690145db70ed59abc5a09f98e8c8e750292101dca1648ee697be6037d3c591184b595d79c6703f0c5320aee62e + languageName: node + linkType: hard + "@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.1, @types/istanbul-lib-coverage@npm:^2.0.6": version: 2.0.6 resolution: "@types/istanbul-lib-coverage@npm:2.0.6" @@ -4726,13 +4963,6 @@ __metadata: languageName: node linkType: hard -"@types/readline-sync@npm:^1.4.8": - version: 1.4.8 - resolution: "@types/readline-sync@npm:1.4.8" - checksum: 9d69fe944d6a26fb1f6a08d205cb9f6107379570ad0b2081df613ccc7414caa026be9307343afcdd3c93b16939ce2e4a76187c33082301a5c7a2bd5d4a7759ee - languageName: node - linkType: hard - "@types/safe-regex@npm:^1.1.6": version: 1.1.6 resolution: "@types/safe-regex@npm:1.1.6" @@ -4815,6 +5045,15 @@ __metadata: languageName: node linkType: hard +"@types/through@npm:*": + version: 0.0.33 + resolution: "@types/through@npm:0.0.33" + dependencies: + "@types/node": "*" + checksum: fd0b73f873a64ed5366d1d757c42e5dbbb2201002667c8958eda7ca02fff09d73de91360572db465ee00240c32d50c6039ea736d8eca374300f9664f93e8da39 + languageName: node + linkType: hard + "@types/triple-beam@npm:^1.3.2": version: 1.3.5 resolution: "@types/triple-beam@npm:1.3.5" @@ -4861,6 +5100,15 @@ __metadata: languageName: node linkType: hard +"@types/ws@npm:^8.18.1": + version: 8.18.1 + resolution: "@types/ws@npm:8.18.1" + dependencies: + "@types/node": "*" + checksum: 0331b14cde388e2805af66cad3e3f51857db8e68ed91e5b99750915e96fe7572e58296dc99999331bbcf08f0ff00a227a0bb214e991f53c2a5aca7b0e71173fa + languageName: node + linkType: hard + "@types/yargs-parser@npm:*": version: 21.0.3 resolution: "@types/yargs-parser@npm:21.0.3" @@ -6574,7 +6822,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^5.4.1": +"chalk@npm:^5.4.1, chalk@npm:^5.6.2": version: 5.6.2 resolution: "chalk@npm:5.6.2" checksum: 4ee2d47a626d79ca27cb5299ecdcce840ef5755e287412536522344db0fc51ca0f6d6433202332c29e2288c6a90a2b31f3bd626bc8c14743b6b6ee28abd3b796 @@ -6588,7 +6836,7 @@ __metadata: languageName: node linkType: hard -"chardet@npm:^2.1.0": +"chardet@npm:^2.1.0, chardet@npm:^2.1.1": version: 2.1.1 resolution: "chardet@npm:2.1.1" checksum: 4e3dba2699018b79bb90a9562b5e5be27fcaab55250c12fa72f026b859fb24846396c346968546c14efc69b9f23aca3ef2b9816775012d08a4686ce3c362415c @@ -6712,6 +6960,15 @@ __metadata: languageName: node linkType: hard +"cli-cursor@npm:^5.0.0": + version: 5.0.0 + resolution: "cli-cursor@npm:5.0.0" + dependencies: + restore-cursor: ^5.0.0 + checksum: 1eb9a3f878b31addfe8d82c6d915ec2330cec8447ab1f117f4aa34f0137fbb3137ec3466e1c9a65bcb7557f6e486d343f2da57f253a2f668d691372dfa15c090 + languageName: node + linkType: hard + "cli-spinners@npm:^2.5.0": version: 2.9.2 resolution: "cli-spinners@npm:2.9.2" @@ -6719,6 +6976,13 @@ __metadata: languageName: node linkType: hard +"cli-spinners@npm:^3.2.0": + version: 3.3.0 + resolution: "cli-spinners@npm:3.3.0" + checksum: c3b9c31d96c9158f4d7140557fffb8c1caea2169d7b895374dd3c2f159267aa0db3b72f36bfcc3bbe3532a7ed162d07dc5c0dc3117e1c0dfe4d387e1d723d616 + languageName: node + linkType: hard + "cli-table3@npm:0.6.5": version: 0.6.5 resolution: "cli-table3@npm:0.6.5" @@ -6913,6 +7177,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^14.0.2": + version: 14.0.2 + resolution: "commander@npm:14.0.2" + checksum: 0a9e549565d368dde2965821833324069b92b099b415c2106996e47db1f0b8c10c77367e9876873c00a52ca627af4c7472eba9b51dc0d6a3ef152ea063d3e9e9 + languageName: node + linkType: hard + "commander@npm:^2.20.0": version: 2.20.3 resolution: "commander@npm:2.20.3" @@ -7514,7 +7785,7 @@ __metadata: languageName: node linkType: hard -"dotenv@npm:17.2.3": +"dotenv@npm:17.2.3, dotenv@npm:^17.2.3": version: 17.2.3 resolution: "dotenv@npm:17.2.3" checksum: fde23eb88649041ec7a0f6a47bbe59cac3c454fc2007cf2e40b9c984aaf0636347218c56cfbbf067034b0a73f530a2698a19b4058695787eb650ec69fe234624 @@ -8713,7 +8984,7 @@ __metadata: languageName: node linkType: hard -"get-east-asian-width@npm:^1.0.0": +"get-east-asian-width@npm:^1.0.0, get-east-asian-width@npm:^1.3.0": version: 1.4.0 resolution: "get-east-asian-width@npm:1.4.0" checksum: 1d9a81a8004f4217ebef5d461875047d269e4b57e039558fd65130877cd4da8e3f61e1c4eada0c8b10e2816c7baf7d5fddb7006f561da13bc6f6dd19c1e964a4 @@ -9229,6 +9500,26 @@ __metadata: languageName: node linkType: hard +"inquirer@npm:^13.0.2": + version: 13.0.2 + resolution: "inquirer@npm:13.0.2" + dependencies: + "@inquirer/ansi": ^2.0.2 + "@inquirer/core": ^11.0.2 + "@inquirer/prompts": ^8.0.2 + "@inquirer/type": ^4.0.2 + mute-stream: ^3.0.0 + run-async: ^4.0.6 + rxjs: ^7.8.2 + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: da44ccb5bec8b7894a9f355b476771599a4aacbe94b4ae9fe1e5b33e7d100debba2ff4de718c338e696a1f408de2c94289c28e75b8e6d147adf675881423fcfc + languageName: node + linkType: hard + "internal-slot@npm:^1.1.0": version: 1.1.0 resolution: "internal-slot@npm:1.1.0" @@ -9450,6 +9741,13 @@ __metadata: languageName: node linkType: hard +"is-interactive@npm:^2.0.0": + version: 2.0.0 + resolution: "is-interactive@npm:2.0.0" + checksum: e8d52ad490bed7ae665032c7675ec07732bbfe25808b0efbc4d5a76b1a1f01c165f332775c63e25e9a03d319ebb6b24f571a9e902669fc1e40b0a60b5be6e26c + languageName: node + linkType: hard + "is-lambda@npm:^1.0.1": version: 1.0.1 resolution: "is-lambda@npm:1.0.1" @@ -9602,7 +9900,7 @@ __metadata: languageName: node linkType: hard -"is-unicode-supported@npm:^2.0.0": +"is-unicode-supported@npm:^2.0.0, is-unicode-supported@npm:^2.1.0": version: 2.1.0 resolution: "is-unicode-supported@npm:2.1.0" checksum: f254e3da6b0ab1a57a94f7273a7798dd35d1d45b227759f600d0fa9d5649f9c07fa8d3c8a6360b0e376adf916d151ec24fc9a50c5295c58bae7ca54a76a063f9 @@ -10683,6 +10981,16 @@ __metadata: languageName: node linkType: hard +"log-symbols@npm:^7.0.1": + version: 7.0.1 + resolution: "log-symbols@npm:7.0.1" + dependencies: + is-unicode-supported: ^2.0.0 + yoctocolors: ^2.1.1 + checksum: 0862313d84826b551582e39659b8586c56b65130c5f4f976420e2c23985228334f2a26fc4251ac22bf0a5b415d9430e86bf332557d934c10b036f9a549d63a09 + languageName: node + linkType: hard + "logform@npm:^2.7.0": version: 2.7.0 resolution: "logform@npm:2.7.0" @@ -10989,7 +11297,7 @@ __metadata: languageName: node linkType: hard -"mimic-function@npm:^5.0.1": +"mimic-function@npm:^5.0.0, mimic-function@npm:^5.0.1": version: 5.0.1 resolution: "mimic-function@npm:5.0.1" checksum: eb5893c99e902ccebbc267c6c6b83092966af84682957f79313311edb95e8bb5f39fb048d77132b700474d1c86d90ccc211e99bae0935447a4834eb4c882982c @@ -11263,6 +11571,13 @@ __metadata: languageName: node linkType: hard +"mute-stream@npm:^3.0.0": + version: 3.0.0 + resolution: "mute-stream@npm:3.0.0" + checksum: bee5db5c996a4585dbffc49e51fea10f3582d7f65441db9bc63126f16269541713c6ccb5a6fe37e08f627967b6eb28dd6b35e54a8dce53cf3837d7e010917b43 + languageName: node + linkType: hard + "mysql2@npm:^3.15.3": version: 3.15.3 resolution: "mysql2@npm:3.15.3" @@ -11666,6 +11981,15 @@ __metadata: languageName: node linkType: hard +"onetime@npm:^7.0.0": + version: 7.0.0 + resolution: "onetime@npm:7.0.0" + dependencies: + mimic-function: ^5.0.0 + checksum: eb08d2da9339819e2f9d52cab9caf2557d80e9af8c7d1ae86e1a0fef027d00a88e9f5bd67494d350df360f7c559fbb44e800b32f310fb989c860214eacbb561c + languageName: node + linkType: hard + "open@npm:^10.1.0": version: 10.2.0 resolution: "open@npm:10.2.0" @@ -11726,6 +12050,23 @@ __metadata: languageName: node linkType: hard +"ora@npm:^9.0.0": + version: 9.0.0 + resolution: "ora@npm:9.0.0" + dependencies: + chalk: ^5.6.2 + cli-cursor: ^5.0.0 + cli-spinners: ^3.2.0 + is-interactive: ^2.0.0 + is-unicode-supported: ^2.1.0 + log-symbols: ^7.0.1 + stdin-discarder: ^0.2.2 + string-width: ^8.1.0 + strip-ansi: ^7.1.2 + checksum: 4efc9c3caa45b552bae4c9755c586eb4f39b824e456d77ec380539529439ca95f3acf9626119131f1ca76618c176088996023e257f5b82e1b86a509990f4545a + languageName: node + linkType: hard + "oracledb@npm:^6.10.0": version: 6.10.0 resolution: "oracledb@npm:6.10.0" @@ -12510,13 +12851,6 @@ __metadata: languageName: node linkType: hard -"readline-sync@npm:^1.4.10": - version: 1.4.10 - resolution: "readline-sync@npm:1.4.10" - checksum: 4dbd8925af028dc4cb1bb813f51ca3479035199aa5224886b560eec8e768ab27d7ebf11d69a67ed93d5a130b7c994f0bdb77796326e563cf928bbfd560e3747e - languageName: node - linkType: hard - "rechoir@npm:^0.8.0": version: 0.8.0 resolution: "rechoir@npm:0.8.0" @@ -12539,7 +12873,7 @@ __metadata: languageName: node linkType: hard -"reflect-metadata@npm:0.2.2, reflect-metadata@npm:^0.2.2": +"reflect-metadata@npm:0.2.2": version: 0.2.2 resolution: "reflect-metadata@npm:0.2.2" checksum: a66c7b583e4efdd8f3c3124fbff33da2d0c86d8280617516308b32b2159af7a3698c961db3246387f56f6316b1d33a608f39bb2b49d813316dfc58f6d3bf3210 @@ -12676,6 +13010,16 @@ __metadata: languageName: node linkType: hard +"restore-cursor@npm:^5.0.0": + version: 5.1.0 + resolution: "restore-cursor@npm:5.1.0" + dependencies: + onetime: ^7.0.0 + signal-exit: ^4.1.0 + checksum: 838dd54e458d89cfbc1a923b343c1b0f170a04100b4ce1733e97531842d7b440463967e521216e8ab6c6f8e89df877acc7b7f4c18ec76e99fb9bf5a60d358d2c + languageName: node + linkType: hard + "ret@npm:~0.5.0": version: 0.5.0 resolution: "ret@npm:0.5.0" @@ -12736,41 +13080,35 @@ __metadata: resolution: "rocketadmin-agent@workspace:rocketadmin-agent" dependencies: "@azure/core-tracing": ^1.3.1 - "@nestjs/cli": ^11.0.10 - "@nestjs/common": ^11.1.8 - "@nestjs/config": ^4.0.2 - "@nestjs/core": ^11.1.8 - "@nestjs/platform-express": ^11.1.8 - "@nestjs/platform-ws": ^11.1.8 - "@nestjs/schematics": ^11.0.9 - "@nestjs/testing": ^11.1.8 "@rocketadmin/shared-code": "workspace:*" - "@types/express": ^5.0.5 + "@types/inquirer": ^9.0.9 "@types/jest": ^30.0.0 "@types/node": ^24.9.1 "@types/pg": ^8.15.6 - "@types/readline-sync": ^1.4.8 "@types/supertest": ^6.0.3 + "@types/ws": ^8.18.1 "@typescript-eslint/eslint-plugin": ^8.46.2 "@typescript-eslint/parser": ^8.46.2 argon2: ^0.44.0 + chalk: ^5.6.2 + commander: ^14.0.2 crypto-js: ^4.2.0 + dotenv: ^17.2.3 eslint: ^9.38.0 eslint-config-prettier: ^10.1.8 eslint-plugin-prettier: ^5.5.4 get-port: ^7.1.0 ibm_db: 3.3.0 + inquirer: ^13.0.2 jest: ^30.2.0 knex: 3.1.0 mongodb: ^6.20.0 mysql2: ^3.15.3 + ora: ^9.0.0 oracledb: ^6.10.0 pg: ^8.16.3 prettier: ^3.6.2 - readline-sync: ^1.4.10 - reflect-metadata: ^0.2.2 rimraf: ^6.0.1 - rxjs: ^7.8.2 ssh2: ^1.17.0 supertest: ^7.1.4 tedious: ^18.6.1 @@ -12781,10 +13119,13 @@ __metadata: typescript: ^5.9.3 wait-on: ^9.0.1 winston: ^3.18.3 + ws: ^8.18.3 yarn: ^1.22.22 dependenciesMeta: ibm_db: optional: true + bin: + rocketadmin-agent: ./dist/main.js languageName: unknown linkType: soft @@ -12816,6 +13157,13 @@ __metadata: languageName: node linkType: hard +"run-async@npm:^4.0.6": + version: 4.0.6 + resolution: "run-async@npm:4.0.6" + checksum: 1338a046d4f4ea03a62dfcb426d44af8c9991221ec74983e52845cbb7ee0c685dc0e9e07cbb6958ee6a1103b7a66c0204b86e110e37909965a92e6fbb7b3b837 + languageName: node + linkType: hard + "run-parallel@npm:^1.1.9": version: 1.2.0 resolution: "run-parallel@npm:1.2.0" @@ -12834,7 +13182,7 @@ __metadata: languageName: node linkType: hard -"rxjs@npm:7.8.2, rxjs@npm:^7.8.2": +"rxjs@npm:7.8.2, rxjs@npm:^7.2.0, rxjs@npm:^7.8.2": version: 7.8.2 resolution: "rxjs@npm:7.8.2" dependencies: @@ -13399,6 +13747,13 @@ __metadata: languageName: node linkType: hard +"stdin-discarder@npm:^0.2.2": + version: 0.2.2 + resolution: "stdin-discarder@npm:0.2.2" + checksum: 642ffd05bd5b100819d6b24a613d83c6e3857c6de74eb02fc51506fa61dc1b0034665163831873868157c4538d71e31762bcf319be86cea04c3aba5336470478 + languageName: node + linkType: hard + "stop-iteration-iterator@npm:^1.1.0": version: 1.1.0 resolution: "stop-iteration-iterator@npm:1.1.0" @@ -13473,6 +13828,16 @@ __metadata: languageName: node linkType: hard +"string-width@npm:^8.1.0": + version: 8.1.0 + resolution: "string-width@npm:8.1.0" + dependencies: + get-east-asian-width: ^1.3.0 + strip-ansi: ^7.1.0 + checksum: 51ee97c4ffee7b94f8a2ee785fac14f81ec9809b9fcec9a4db44e25c717c263af0cc4387c111aef76195c0718dc43766f3678c07fb542294fb0244f7bfbde883 + languageName: node + linkType: hard + "string.prototype.trim@npm:^1.2.10": version: 1.2.10 resolution: "string.prototype.trim@npm:1.2.10" @@ -13538,7 +13903,7 @@ __metadata: languageName: node linkType: hard -"strip-ansi@npm:^7.0.1, strip-ansi@npm:^7.1.0": +"strip-ansi@npm:^7.0.1, strip-ansi@npm:^7.1.0, strip-ansi@npm:^7.1.2": version: 7.1.2 resolution: "strip-ansi@npm:7.1.2" dependencies: @@ -15007,6 +15372,17 @@ __metadata: languageName: node linkType: hard +"wrap-ansi@npm:^9.0.2": + version: 9.0.2 + resolution: "wrap-ansi@npm:9.0.2" + dependencies: + ansi-styles: ^6.2.1 + string-width: ^7.0.0 + strip-ansi: ^7.1.0 + checksum: 9827bf8bbb341d2d15f26d8507d98ca2695279359073422fe089d374b30e233d24ab95beca55cf9ab8dcb89face00e919be4158af50d4b6d8eab5ef4ee399e0c + languageName: node + linkType: hard + "wrappy@npm:1": version: 1.0.2 resolution: "wrappy@npm:1.0.2" @@ -15034,7 +15410,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:8.18.3": +"ws@npm:^8.18.3": version: 8.18.3 resolution: "ws@npm:8.18.3" peerDependencies: