diff --git a/apps/server/package.json b/apps/server/package.json index 55885d2..9d9982f 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -44,6 +44,7 @@ "@nestjs/passport": "^10.0.3", "@nestjs/platform-fastify": "^10.3.9", "@nestjs/platform-socket.io": "^10.3.9", + "@nestjs/terminus": "^10.2.3", "@nestjs/websockets": "^10.3.9", "@react-email/components": "0.0.19", "@react-email/render": "^0.0.15", diff --git a/apps/server/src/app.module.ts b/apps/server/src/app.module.ts index 2d1a413..2883272 100644 --- a/apps/server/src/app.module.ts +++ b/apps/server/src/app.module.ts @@ -11,6 +11,7 @@ import { MailModule } from './integrations/mail/mail.module'; import { QueueModule } from './integrations/queue/queue.module'; import { StaticModule } from './integrations/static/static.module'; import { EventEmitterModule } from '@nestjs/event-emitter'; +import { HealthModule } from './integrations/health/health.module'; @Module({ imports: [ @@ -21,6 +22,7 @@ import { EventEmitterModule } from '@nestjs/event-emitter'; WsModule, QueueModule, StaticModule, + HealthModule, StorageModule.forRootAsync({ imports: [EnvironmentModule], }), diff --git a/apps/server/src/common/interceptors/http-response.interceptor.ts b/apps/server/src/common/interceptors/http-response.interceptor.ts index 22484ea..5a20501 100644 --- a/apps/server/src/common/interceptors/http-response.interceptor.ts +++ b/apps/server/src/common/interceptors/http-response.interceptor.ts @@ -16,7 +16,15 @@ export class TransformHttpResponseInterceptor intercept( context: ExecutionContext, next: CallHandler, - ): Observable> { + ): Observable | any> { + const request = context.switchToHttp().getRequest(); + const path = request.url; + + // Skip interceptor for the /api/health path + if (path === '/api/health') { + return next.handle(); + } + return next.handle().pipe( map((data) => { const status = context.switchToHttp().getResponse().statusCode; diff --git a/apps/server/src/core/core.module.ts b/apps/server/src/core/core.module.ts index d97e368..1764463 100644 --- a/apps/server/src/core/core.module.ts +++ b/apps/server/src/core/core.module.ts @@ -34,7 +34,10 @@ export class CoreModule implements NestModule { configure(consumer: MiddlewareConsumer) { consumer .apply(DomainMiddleware) - .exclude({ path: 'auth/setup', method: RequestMethod.POST }) + .exclude( + { path: 'auth/setup', method: RequestMethod.POST }, + { path: 'health', method: RequestMethod.GET }, + ) .forRoutes('*'); } } diff --git a/apps/server/src/database/database.module.ts b/apps/server/src/database/database.module.ts index 0b0ba87..7ce7eb9 100644 --- a/apps/server/src/database/database.module.ts +++ b/apps/server/src/database/database.module.ts @@ -36,6 +36,8 @@ types.setTypeParser(types.builtins.INT8, (val) => Number(val)); dialect: new PostgresDialect({ pool: new Pool({ connectionString: environmentService.getDatabaseURL(), + }).on('error', (err) => { + console.error('Database error:', err.message); }), }), plugins: [new CamelCasePlugin()], @@ -102,7 +104,7 @@ export class DatabaseModule implements OnModuleDestroy, OnApplicationBootstrap { } async establishConnection() { - const retryAttempts = 10; + const retryAttempts = 15; const retryDelay = 3000; this.logger.log('Establishing database connection'); diff --git a/apps/server/src/integrations/health/health.controller.ts b/apps/server/src/integrations/health/health.controller.ts new file mode 100644 index 0000000..c33214e --- /dev/null +++ b/apps/server/src/integrations/health/health.controller.ts @@ -0,0 +1,22 @@ +import { Controller, Get } from '@nestjs/common'; +import { HealthCheck, HealthCheckService } from '@nestjs/terminus'; +import { PostgresHealthIndicator } from './postgres.health'; +import { RedisHealthIndicator } from './redis.health'; + +@Controller('health') +export class HealthController { + constructor( + private health: HealthCheckService, + private postgres: PostgresHealthIndicator, + private redis: RedisHealthIndicator, + ) {} + + @Get() + @HealthCheck() + async check() { + return this.health.check([ + () => this.postgres.pingCheck('database'), + () => this.redis.pingCheck('redis'), + ]); + } +} diff --git a/apps/server/src/integrations/health/health.module.ts b/apps/server/src/integrations/health/health.module.ts new file mode 100644 index 0000000..37b410c --- /dev/null +++ b/apps/server/src/integrations/health/health.module.ts @@ -0,0 +1,13 @@ +import { Global, Module } from '@nestjs/common'; +import { HealthController } from './health.controller'; +import { TerminusModule } from '@nestjs/terminus'; +import { PostgresHealthIndicator } from './postgres.health'; +import { RedisHealthIndicator } from './redis.health'; + +@Global() +@Module({ + controllers: [HealthController], + providers: [PostgresHealthIndicator, RedisHealthIndicator], + imports: [TerminusModule], +}) +export class HealthModule {} diff --git a/apps/server/src/integrations/health/postgres.health.ts b/apps/server/src/integrations/health/postgres.health.ts new file mode 100644 index 0000000..07b4cb3 --- /dev/null +++ b/apps/server/src/integrations/health/postgres.health.ts @@ -0,0 +1,31 @@ +import { InjectKysely } from 'nestjs-kysely'; +import { + HealthCheckError, + HealthIndicator, + HealthIndicatorResult, +} from '@nestjs/terminus'; +import { Injectable, Logger } from '@nestjs/common'; +import { sql } from 'kysely'; +import { KyselyDB } from '@docmost/db/types/kysely.types'; + +@Injectable() +export class PostgresHealthIndicator extends HealthIndicator { + private readonly logger = new Logger(PostgresHealthIndicator.name); + + constructor(@InjectKysely() private readonly db: KyselyDB) { + super(); + } + + async pingCheck(key: string): Promise { + try { + await sql`SELECT 1=1`.execute(this.db); + return this.getStatus(key, true); + } catch (e) { + this.logger.error(JSON.stringify(e)); + throw new HealthCheckError( + `${key} is not available`, + this.getStatus(key, false), + ); + } + } +} diff --git a/apps/server/src/integrations/health/redis.health.ts b/apps/server/src/integrations/health/redis.health.ts new file mode 100644 index 0000000..1b4e1f6 --- /dev/null +++ b/apps/server/src/integrations/health/redis.health.ts @@ -0,0 +1,34 @@ +import { + HealthCheckError, + HealthIndicator, + HealthIndicatorResult, +} from '@nestjs/terminus'; +import { Injectable, Logger } from '@nestjs/common'; +import { EnvironmentService } from '../environment/environment.service'; +import { Redis } from 'ioredis'; + +@Injectable() +export class RedisHealthIndicator extends HealthIndicator { + private readonly logger = new Logger(RedisHealthIndicator.name); + + constructor(private environmentService: EnvironmentService) { + super(); + } + + async pingCheck(key: string): Promise { + try { + const redis = new Redis(this.environmentService.getRedisUrl(), { + maxRetriesPerRequest: 15, + }); + + await redis.ping(); + return this.getStatus(key, true); + } catch (e) { + this.logger.error(e); + throw new HealthCheckError( + `${key} is not available`, + this.getStatus(key, false), + ); + } + } +} diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index c3e180a..62fa470 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -40,7 +40,8 @@ async function bootstrap() { .addHook('preHandler', function (req, reply, done) { if ( req.originalUrl.startsWith('/api') && - !req.originalUrl.startsWith('/api/auth/setup') + !req.originalUrl.startsWith('/api/auth/setup') && + !req.originalUrl.startsWith('/api/health') ) { if (!req.raw?.['workspaceId']) { throw new NotFoundException('Workspace not found'); diff --git a/package.json b/package.json index 6f69b82..c41a8bd 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "client:dev": "nx run client:dev", "server:dev": "nx run server:start:dev", "server:start": "nx run server:start:prod", - "email:dev": "nx run @docmost/transactional:dev" + "email:dev": "nx run @docmost/transactional:dev", + "dev": "pnpm concurrently -n \"frontend,backend\" -c \"cyan,green\" \"pnpm run client:dev\" \"pnpm run server:dev\"" }, "dependencies": { "@docmost/editor-ext": "workspace:*", @@ -66,6 +67,7 @@ "@nx/js": "19.3.2", "@types/uuid": "^10.0.0", "nx": "19.3.2", + "concurrently": "^8.2.2", "tsx": "^4.15.7" }, "workspaces": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6905a1c..c4d2a9d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -153,6 +153,9 @@ importers: '@types/uuid': specifier: ^10.0.0 version: 10.0.0 + concurrently: + specifier: ^8.2.2 + version: 8.2.2 nx: specifier: 19.3.2 version: 19.3.2(@swc/core@1.5.25(@swc/helpers@0.5.11)) @@ -370,6 +373,9 @@ importers: '@nestjs/platform-socket.io': specifier: ^10.3.9 version: 10.3.9(@nestjs/common@10.3.9(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.3.9)(rxjs@7.8.1) + '@nestjs/terminus': + specifier: ^10.2.3 + version: 10.2.3(@nestjs/common@10.3.9(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.9(@nestjs/common@10.3.9(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.3.9)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/websockets': specifier: ^10.3.9 version: 10.3.9(@nestjs/common@10.3.9(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.9)(@nestjs/platform-socket.io@10.3.9)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -2289,6 +2295,54 @@ packages: peerDependencies: typescript: '>=4.8.2' + '@nestjs/terminus@10.2.3': + resolution: {integrity: sha512-iX7gXtAooePcyQqFt57aDke5MzgdkBeYgF5YsFNNFwOiAFdIQEhfv3PR0G+HlH9F6D7nBCDZt9U87Pks/qHijg==} + peerDependencies: + '@grpc/grpc-js': '*' + '@grpc/proto-loader': '*' + '@mikro-orm/core': '*' + '@mikro-orm/nestjs': '*' + '@nestjs/axios': ^1.0.0 || ^2.0.0 || ^3.0.0 + '@nestjs/common': ^9.0.0 || ^10.0.0 + '@nestjs/core': ^9.0.0 || ^10.0.0 + '@nestjs/microservices': ^9.0.0 || ^10.0.0 + '@nestjs/mongoose': ^9.0.0 || ^10.0.0 + '@nestjs/sequelize': ^9.0.0 || ^10.0.0 + '@nestjs/typeorm': ^9.0.0 || ^10.0.0 + '@prisma/client': '*' + mongoose: '*' + reflect-metadata: 0.1.x || 0.2.x + rxjs: 7.x + sequelize: '*' + typeorm: '*' + peerDependenciesMeta: + '@grpc/grpc-js': + optional: true + '@grpc/proto-loader': + optional: true + '@mikro-orm/core': + optional: true + '@mikro-orm/nestjs': + optional: true + '@nestjs/axios': + optional: true + '@nestjs/microservices': + optional: true + '@nestjs/mongoose': + optional: true + '@nestjs/sequelize': + optional: true + '@nestjs/typeorm': + optional: true + '@prisma/client': + optional: true + mongoose: + optional: true + sequelize: + optional: true + typeorm: + optional: true + '@nestjs/testing@10.3.9': resolution: {integrity: sha512-z24SdpZIRtYyM5s2vnu7rbBosXJY/KcAP7oJlwgFa/h/z/wg8gzyoKy5lhibH//OZNO+pYKajV5wczxuy5WeAg==} peerDependencies: @@ -4164,6 +4218,9 @@ packages: ajv@8.12.0: resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -4349,6 +4406,10 @@ packages: bowser@2.11.0: resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==} + boxen@5.1.2: + resolution: {integrity: sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==} + engines: {node: '>=10'} + brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -4443,6 +4504,10 @@ packages: chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + check-disk-space@3.4.0: + resolution: {integrity: sha512-drVkSqfwA+TvuEhFipiR1OC9boEGZL5RrWvVsOthdcvQNXyCCuKkEiTOTXZ7qxSf/GLwq4GvzfrQD/Wz325hgw==} + engines: {node: '>=16'} + chokidar@3.5.3: resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} engines: {node: '>= 8.10.0'} @@ -4472,6 +4537,10 @@ packages: class-validator@0.14.1: resolution: {integrity: sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==} + cli-boxes@2.2.1: + resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==} + engines: {node: '>=6'} + cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} @@ -4584,6 +4653,11 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + concurrently@8.2.2: + resolution: {integrity: sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==} + engines: {node: ^14.13.0 || >=16.0.0} + hasBin: true + config-chain@1.1.13: resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} @@ -4686,6 +4760,10 @@ packages: dash-get@1.0.2: resolution: {integrity: sha512-4FbVrHDwfOASx7uQVxeiCTo7ggSdYZbqs8lH+WU6ViypPlDbe9y6IP5VVUDQBv9DcnyaiPT5XT0UWHgJ64zLeQ==} + date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + date-fns@3.6.0: resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} @@ -7191,6 +7269,9 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shell-quote@1.8.1: + resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} + shelljs.exec@1.1.8: resolution: {integrity: sha512-vFILCw+lzUtiwBAHV8/Ex8JsFjelFMdhONIsgKNLgTzeRckp2AOYRQtHJE/9LhNvdMmE27AGtzWx0+DHpwIwSw==} engines: {node: '>= 4.0.0'} @@ -7275,6 +7356,9 @@ packages: resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} engines: {node: '>= 8'} + spawn-command@0.0.2: + resolution: {integrity: sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==} + split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} @@ -7608,6 +7692,10 @@ packages: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + type-fest@0.21.3: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} @@ -7850,6 +7938,10 @@ packages: wide-align@1.1.5: resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + widest-line@3.1.0: + resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} + engines: {node: '>=8'} + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -10404,6 +10496,15 @@ snapshots: transitivePeerDependencies: - chokidar + '@nestjs/terminus@10.2.3(@nestjs/common@10.3.9(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.9(@nestjs/common@10.3.9(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.3.9)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1)': + dependencies: + '@nestjs/common': 10.3.9(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.3.9(@nestjs/common@10.3.9(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.3.9)(reflect-metadata@0.2.2)(rxjs@7.8.1) + boxen: 5.1.2 + check-disk-space: 3.4.0 + reflect-metadata: 0.2.2 + rxjs: 7.8.1 + '@nestjs/testing@10.3.9(@nestjs/common@10.3.9(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.9(@nestjs/common@10.3.9(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.3.9)(reflect-metadata@0.2.2)(rxjs@7.8.1))': dependencies: '@nestjs/common': 10.3.9(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -12466,6 +12567,10 @@ snapshots: require-from-string: 2.0.2 uri-js: 4.4.1 + ansi-align@3.0.1: + dependencies: + string-width: 4.2.3 + ansi-colors@4.1.3: {} ansi-escapes@4.3.2: @@ -12721,6 +12826,17 @@ snapshots: bowser@2.11.0: {} + boxen@5.1.2: + dependencies: + ansi-align: 3.0.1 + camelcase: 6.3.0 + chalk: 4.1.2 + cli-boxes: 2.2.1 + string-width: 4.2.3 + type-fest: 0.20.2 + widest-line: 3.1.0 + wrap-ansi: 7.0.0 + brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 @@ -12822,6 +12938,8 @@ snapshots: chardet@0.7.0: {} + check-disk-space@3.4.0: {} + chokidar@3.5.3: dependencies: anymatch: 3.1.3 @@ -12862,6 +12980,8 @@ snapshots: libphonenumber-js: 1.10.58 validator: 13.12.0 + cli-boxes@2.2.1: {} + cli-cursor@3.1.0: dependencies: restore-cursor: 3.1.0 @@ -12947,6 +13067,18 @@ snapshots: concat-map@0.0.1: {} + concurrently@8.2.2: + dependencies: + chalk: 4.1.2 + date-fns: 2.30.0 + lodash: 4.17.21 + rxjs: 7.8.1 + shell-quote: 1.8.1 + spawn-command: 0.0.2 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.2 + config-chain@1.1.13: dependencies: ini: 1.3.8 @@ -13053,6 +13185,10 @@ snapshots: dash-get@1.0.2: {} + date-fns@2.30.0: + dependencies: + '@babel/runtime': 7.23.7 + date-fns@3.6.0: {} debounce@2.0.0: {} @@ -15963,6 +16099,8 @@ snapshots: shebang-regex@3.0.0: {} + shell-quote@1.8.1: {} + shelljs.exec@1.1.8: {} shelljs@0.8.5: @@ -16084,6 +16222,8 @@ snapshots: source-map@0.7.4: {} + spawn-command@0.0.2: {} + split2@4.2.0: {} sprintf-js@1.0.3: {} @@ -16451,6 +16591,8 @@ snapshots: type-detect@4.0.8: {} + type-fest@0.20.2: {} + type-fest@0.21.3: {} type-fest@0.7.1: {} @@ -16668,6 +16810,10 @@ snapshots: dependencies: string-width: 4.2.3 + widest-line@3.1.0: + dependencies: + string-width: 4.2.3 + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0