diff --git a/.github/workflows/ci-cli.yaml b/.github/workflows/ci-cli.yaml index c357e0a82c0..80848461293 100644 --- a/.github/workflows/ci-cli.yaml +++ b/.github/workflows/ci-cli.yaml @@ -20,6 +20,7 @@ jobs: with: files: | packages/twenty-cli/** + packages/twenty-server/** cli-test: needs: changed-files-check if: needs.changed-files-check.outputs.any_changed == 'true' @@ -44,11 +45,52 @@ jobs: with: tag: scope:cli tasks: ${{ matrix.task }} + cli-e2e-test: + timeout-minutes: 30 + runs-on: depot-ubuntu-24.04-8 + needs: [changed-files-check, cli-test] + if: needs.changed-files-check.outputs.any_changed == 'true' + services: + postgres: + image: twentycrm/twenty-postgres-spilo + env: + PGUSER_SUPERUSER: postgres + PGPASSWORD_SUPERUSER: postgres + ALLOW_NOSSL: 'true' + SPILO_PROVIDER: 'local' + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + redis: + image: redis + ports: + - 6379:6379 + env: + NODE_ENV: test + steps: + - name: Fetch custom Github Actions and base branch history + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install dependencies + uses: ./.github/workflows/actions/yarn-install + - name: Server / Create Test DB + run: | + PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";' + - name: CLI / Run E2E Tests + uses: ./.github/workflows/actions/nx-affected + with: + tag: scope:cli + tasks: 'test:e2e' ci-cli-status-check: if: always() && !cancelled() timeout-minutes: 5 runs-on: ubuntu-latest - needs: [changed-files-check, cli-test] + needs: [changed-files-check, cli-test, cli-e2e-test] steps: - name: Fail job if any needs failed if: contains(needs.*.result, 'failure') diff --git a/packages/twenty-cli/.env.e2e b/packages/twenty-cli/.env.e2e new file mode 100644 index 00000000000..0f92e2be146 --- /dev/null +++ b/packages/twenty-cli/.env.e2e @@ -0,0 +1,4 @@ +TWENTY_API_URL=http://localhost:3000 +# APPLE_JANE_ADMIN_ACCESS_TOKEN +TWENTY_API_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik +TWENTY_DEFAULT_APP=e2e-default-app diff --git a/packages/twenty-cli/jest.e2e.config.ts b/packages/twenty-cli/jest.e2e.config.ts new file mode 100644 index 00000000000..a5c8c433964 --- /dev/null +++ b/packages/twenty-cli/jest.e2e.config.ts @@ -0,0 +1,48 @@ +import { type JestConfigWithTsJest } from 'ts-jest'; + +const jestConfig: JestConfigWithTsJest = { + // For more information please have a look to official docs https://jestjs.io/docs/configuration/#prettierpath-string + // Prettier v3 should be supported in jest v30 https://github.com/jestjs/jest/releases/tag/v30.0.0-alpha.1 + prettierPath: null, + displayName: 'twenty-cli-e2e', + silent: false, + errorOnDeprecated: true, + maxConcurrency: 1, + moduleFileExtensions: ['js', 'json', 'ts'], + rootDir: '.', + testEnvironment: 'node', + testRegex: '\\.e2e-spec\\.ts$', + modulePathIgnorePatterns: ['/dist'], + globalTeardown: '/src/__tests__/e2e/teardown.ts', + testTimeout: 30000, // 30 seconds timeout for e2e tests + maxWorkers: 1, + transform: { + '^.+\\.(t|j)s$': [ + '@swc/jest', + { + jsc: { + parser: { + syntax: 'typescript', + tsx: false, + decorators: true, + }, + transform: { + decoratorMetadata: true, + }, + baseUrl: '.', + paths: { + 'src/*': ['./src/*'], + }, + }, + }, + ], + }, + transformIgnorePatterns: [ + 'node_modules/(?!(chalk|inquirer|@inquirer|ansi-styles|strip-ansi|has-flag|supports-color|color-convert|color-name|wrap-ansi|string-width|is-fullwidth-code-point|emoji-regex|onetime|mimic-fn|signal-exit|yallist|lru-cache|p-limit|p-queue|p-timeout|p-finally|p-try|p-cancelable|p-locate|p-map|p-race|p-reduce|p-some|p-waterfall|p-defer|p-delay|p-retry|p-any|p-settle|p-all|p-map-series|p-map-concurrent|p-filter|p-reject|p-tap|p-log|p-debounce|p-throttle|p-forever|p-whilst|p-do-whilst|p-until|p-wait-for|p-min-delay)/)', + ], + moduleNameMapper: { + '^@/(.*)$': '/src/$1', + }, +}; + +export default jestConfig; diff --git a/packages/twenty-cli/package.json b/packages/twenty-cli/package.json index e2188757cfa..83714984894 100644 --- a/packages/twenty-cli/package.json +++ b/packages/twenty-cli/package.json @@ -6,6 +6,11 @@ "bin": { "twenty": "dist/cli.js" }, + "files": [ + "dist/**/*", + "!dist/**/*.e2e-spec.*", + "!dist/**/__tests__/**" + ], "scripts": { "build": "echo 'use npx nx build'", "dev": "tsx src/cli.ts", @@ -40,7 +45,8 @@ "@types/node": "^20.0.0", "jest": "^29.5.0", "tsx": "^4.7.0", - "typescript": "^5.3.0" + "typescript": "^5.3.0", + "wait-on": "^7.2.0" }, "engines": { "node": "^24.5.0", diff --git a/packages/twenty-cli/project.json b/packages/twenty-cli/project.json index 285e23b4ce9..065bbb05020 100644 --- a/packages/twenty-cli/project.json +++ b/packages/twenty-cli/project.json @@ -9,7 +9,7 @@ "cache": true, "options": { "cwd": "packages/twenty-cli", - "commands": ["rimraf dist", "tsc"] + "commands": ["rimraf dist", "tsc --project tsconfig.lib.json"] }, "dependsOn": ["^after-build"] }, @@ -18,7 +18,9 @@ "cache": true, "options": { "cwd": "packages/twenty-cli", - "commands": ["cp -R src/constants/base-application-project dist/constants"] + "commands": [ + "cp -R src/constants/base-application-project dist/constants" + ] }, "dependsOn": ["after-build"] }, @@ -42,7 +44,7 @@ "executor": "nx:run-commands", "options": { "cwd": "packages/twenty-cli", - "command": "tsc --noEmit" + "command": "tsc --noEmit --project tsconfig.lib.json" } }, "lint": { @@ -51,7 +53,7 @@ "maxWarnings": 0 }, "configurations": { - "ci": { + "ci": { "lintFilePatterns": ["{projectRoot}/src/**/*.{ts,json}"], "maxWarnings": 0 }, @@ -71,6 +73,31 @@ "watchAll": false } } + }, + "test:e2e": { + "executor": "nx:run-commands", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "cwd": "packages/twenty-cli", + "commands": [ + "npx wait-on http://localhost:3000/healthz --timeout 600000 --interval 1000 --verbose && NODE_ENV=test npx jest --config ./jest.e2e.config.ts" + ] + }, + "parallel": false, + "dependsOn": [ + { + "target": "build", + "projects": "dependencies" + }, + { + "target": "database:reset", + "projects": "twenty-server" + }, + { + "target": "start:ci-if-needed", + "projects": "twenty-server" + } + ] } } } diff --git a/packages/twenty-cli/src/__tests__/e2e/applications-install-delete-reinstall.e2e-spec.ts b/packages/twenty-cli/src/__tests__/e2e/applications-install-delete-reinstall.e2e-spec.ts new file mode 100644 index 00000000000..94d425efa28 --- /dev/null +++ b/packages/twenty-cli/src/__tests__/e2e/applications-install-delete-reinstall.e2e-spec.ts @@ -0,0 +1,52 @@ +import { existsSync } from 'fs'; +import { AppDeleteCommand } from '../../commands/app-delete.command'; +import { AppSyncCommand } from '../../commands/app-sync.command'; +import { COVERED_APPLICATION_FOLDERS } from './constants/covered-applications-folder.constant'; +import { getTestedApplicationPath } from './utils/get-tested-application-path.util'; + +describe.each(COVERED_APPLICATION_FOLDERS)( + 'Application: "%s" install delete and reinstall test suite', + (applicationName) => { + const syncCommand = new AppSyncCommand(); + const deleteCommand = new AppDeleteCommand(); + const appPath = getTestedApplicationPath(applicationName); + + beforeAll(async () => { + expect(existsSync(appPath)).toBe(true); + }); + + // TODO uncomment when reinstall is fix to cleanup gracefully + // afterAll(async () => { + // const result = await deleteCommand.execute({ + // appPath, + // askForConfirmation: false, + // }); + + // expect(result.success).toBe(true); + // }); + + it(`should successfully install ${applicationName} application`, async () => { + const result = await syncCommand.execute(appPath); + + expect(result.success).toBe(true); + }); + + it(`should successfully delete ${applicationName} application`, async () => { + const result = await deleteCommand.execute({ + appPath, + askForConfirmation: false, + }); + + expect(result.success).toBe(true); + }); + + it.failing( + `should successfully re-install ${applicationName} application`, + async () => { + const result = await syncCommand.execute(appPath); + + expect(result.success).toBe(true); + }, + ); + }, +); diff --git a/packages/twenty-cli/src/__tests__/e2e/constants/covered-applications-folder.constant.ts b/packages/twenty-cli/src/__tests__/e2e/constants/covered-applications-folder.constant.ts new file mode 100644 index 00000000000..569ce1bd826 --- /dev/null +++ b/packages/twenty-cli/src/__tests__/e2e/constants/covered-applications-folder.constant.ts @@ -0,0 +1 @@ +export const COVERED_APPLICATION_FOLDERS = ['hello-world'] as const; diff --git a/packages/twenty-cli/src/__tests__/e2e/constants/server-url.constant.ts b/packages/twenty-cli/src/__tests__/e2e/constants/server-url.constant.ts new file mode 100644 index 00000000000..5e8aa7f2818 --- /dev/null +++ b/packages/twenty-cli/src/__tests__/e2e/constants/server-url.constant.ts @@ -0,0 +1 @@ +export const SERVER_URL = 'http://localhost:3000'; diff --git a/packages/twenty-cli/src/__tests__/e2e/health.e2e-spec.ts b/packages/twenty-cli/src/__tests__/e2e/health.e2e-spec.ts new file mode 100644 index 00000000000..f7f012e88d8 --- /dev/null +++ b/packages/twenty-cli/src/__tests__/e2e/health.e2e-spec.ts @@ -0,0 +1,21 @@ +import axios from 'axios'; +import { ConfigService } from '../../services/config.service'; +import { SERVER_URL } from './constants/server-url.constant'; + +describe('Twenty Server Health Check (E2E)', () => { + const configService = new ConfigService(); + const HEALTH_ENDPOINT = `${SERVER_URL}/healthz`; + + it('should assert e2e configuration is loaded by default', async () => { + const configuration = await configService.getConfig(); + expect(configuration).toMatchObject({ + defaultApp: 'e2e-default-app', + }); + }); + + it('should return 200 for health', async () => { + const response = await axios.get(HEALTH_ENDPOINT); + expect(response.status).toBe(200); + expect(response.data).toBeDefined(); + }); +}); diff --git a/packages/twenty-cli/src/__tests__/e2e/teardown.ts b/packages/twenty-cli/src/__tests__/e2e/teardown.ts new file mode 100644 index 00000000000..f5a431d9b5d --- /dev/null +++ b/packages/twenty-cli/src/__tests__/e2e/teardown.ts @@ -0,0 +1,14 @@ +import { exec } from 'child_process'; + +export default async function globalTeardown() { + return new Promise((resolve) => { + exec('pkill -f "nest start" || true', (error: unknown) => { + if (error) { + console.log('No server processes to kill'); + } else { + console.log('✅ Server processes cleaned up'); + } + resolve(); + }); + }); +} diff --git a/packages/twenty-cli/src/__tests__/e2e/utils/get-tested-application-path.util.ts b/packages/twenty-cli/src/__tests__/e2e/utils/get-tested-application-path.util.ts new file mode 100644 index 00000000000..e00b351e302 --- /dev/null +++ b/packages/twenty-cli/src/__tests__/e2e/utils/get-tested-application-path.util.ts @@ -0,0 +1,12 @@ +import path from 'path'; + +export const getTestedApplicationPath = (relativePath: string): string => { + const currentFileDir = __dirname; + + const twentyAppsPath = path.resolve( + currentFileDir, + '../../../../../twenty-apps', + ); + + return path.join(twentyAppsPath, relativePath); +}; diff --git a/packages/twenty-cli/src/commands/app-delete.command.ts b/packages/twenty-cli/src/commands/app-delete.command.ts index 8e782b57e24..5216343b791 100644 --- a/packages/twenty-cli/src/commands/app-delete.command.ts +++ b/packages/twenty-cli/src/commands/app-delete.command.ts @@ -1,21 +1,26 @@ -import inquirer from 'inquirer'; import chalk from 'chalk'; -import { ApiService } from '../services/api.service'; +import inquirer from 'inquirer'; import { CURRENT_EXECUTION_DIRECTORY } from '../constants/current-execution-directory'; +import { ApiService } from '../services/api.service'; +import { ApiResponse } from '../types/config.types'; import { loadManifest } from '../utils/app-manifest-loader'; export class AppDeleteCommand { private apiService = new ApiService(); - async execute(): Promise { + async execute({ + appPath = CURRENT_EXECUTION_DIRECTORY, + askForConfirmation, + }: { + appPath?: string; + askForConfirmation: boolean; + }): Promise> { try { - const appPath = CURRENT_EXECUTION_DIRECTORY; - console.log(chalk.blue('🚀 Deleting Twenty Application')); console.log(chalk.gray(`📁 App Path: ${appPath}`)); console.log(''); - if (!(await this.confirmationPrompt())) { + if (askForConfirmation && !(await this.confirmationPrompt())) { console.error(chalk.red('⛔️ Aborting deletion')); process.exit(1); } @@ -26,16 +31,17 @@ export class AppDeleteCommand { if (!result.success) { console.error(chalk.red('❌ Deletion failed:'), result.error); - process.exit(1); + } else { + console.log(chalk.green('✅ Application deleted successfully')); } - console.log(chalk.green('✅ Application deleted successfully')); + return result; } catch (error) { console.error( chalk.red('Deletion failed:'), error instanceof Error ? error.message : error, ); - process.exit(1); + throw error; } } diff --git a/packages/twenty-cli/src/commands/app-sync.command.ts b/packages/twenty-cli/src/commands/app-sync.command.ts index 9b48676dda8..413581a7e69 100644 --- a/packages/twenty-cli/src/commands/app-sync.command.ts +++ b/packages/twenty-cli/src/commands/app-sync.command.ts @@ -1,15 +1,17 @@ import chalk from 'chalk'; -import { ApiService } from '../services/api.service'; import { CURRENT_EXECUTION_DIRECTORY } from '../constants/current-execution-directory'; +import { ApiService } from '../services/api.service'; +import { ApiResponse } from '../types/config.types'; import { loadManifest } from '../utils/app-manifest-loader'; export class AppSyncCommand { private apiService = new ApiService(); - async execute(): Promise { + // TODO improve typing + async execute( + appPath: string = CURRENT_EXECUTION_DIRECTORY, + ): Promise> { try { - const appPath = CURRENT_EXECUTION_DIRECTORY; - console.log(chalk.blue('🚀 Syncing Twenty Application')); console.log(chalk.gray(`📁 App Path: ${appPath}`)); console.log(''); @@ -24,16 +26,17 @@ export class AppSyncCommand { if (!result.success) { console.error(chalk.red('❌ Sync failed:'), result.error); - process.exit(1); + } else { + console.log(chalk.green('✅ Application synced successfully')); } - console.log(chalk.green('✅ Application synced successfully')); + return result; } catch (error) { console.error( chalk.red('Sync failed:'), error instanceof Error ? error.message : error, ); - process.exit(1); + throw error; } } } diff --git a/packages/twenty-cli/src/commands/app.command.ts b/packages/twenty-cli/src/commands/app.command.ts index b5116684944..06f09c5b7a7 100644 --- a/packages/twenty-cli/src/commands/app.command.ts +++ b/packages/twenty-cli/src/commands/app.command.ts @@ -1,14 +1,14 @@ +import chalk from 'chalk'; import { Command } from 'commander'; -import { AppSyncCommand } from './app-sync.command'; -import { AppDevCommand } from './app-dev.command'; -import { AppInitCommand } from './app-init.command'; -import { AppDeleteCommand } from './app-delete.command'; import { AppAddCommand, isSyncableEntity, SyncableEntity, } from './app-add.command'; -import chalk from 'chalk'; +import { AppDeleteCommand } from './app-delete.command'; +import { AppDevCommand } from './app-dev.command'; +import { AppInitCommand } from './app-init.command'; +import { AppSyncCommand } from './app-sync.command'; export class AppCommand { private devCommand = new AppDevCommand(); @@ -33,14 +33,30 @@ export class AppCommand { .command('sync') .description('Sync application to Twenty') .action(async () => { - await this.syncCommand.execute(); + try { + const result = await this.syncCommand.execute(); + if (!result.success) { + process.exit(1); + } + } catch { + process.exit(1); + } }); appCommand .command('delete') .description('Delete application from Twenty') .action(async () => { - await this.deleteCommand.execute(); + try { + const result = await this.deleteCommand.execute({ + askForConfirmation: true, + }); + if (!result.success) { + process.exit(1); + } + } catch { + process.exit(1); + } }); appCommand diff --git a/packages/twenty-cli/src/services/config.service.ts b/packages/twenty-cli/src/services/config.service.ts index ccc9d0fcb57..8707efcd18a 100644 --- a/packages/twenty-cli/src/services/config.service.ts +++ b/packages/twenty-cli/src/services/config.service.ts @@ -14,7 +14,10 @@ export class ConfigService { private loadEnvironmentVariables(): void { // Load local .env file if it exists in current working directory - const localEnvPath = path.join(process.cwd(), '.env'); + const localEnvPath = path.join( + process.cwd(), + process.env.NODE_ENV === 'test' ? '.env.e2e' : '.env', + ); if (fs.existsSync(localEnvPath)) { loadDotenv({ path: localEnvPath }); } @@ -38,6 +41,7 @@ export class ConfigService { if (configExists) { const configContent = await fs.readFile(this.configPath, 'utf8'); + // TODO parse using a zod schema fileConfig = JSON.parse(configContent || '{}'); } diff --git a/packages/twenty-cli/src/utils/app-manifest-loader.ts b/packages/twenty-cli/src/utils/app-manifest-loader.ts index e5a85d5895e..610206708c9 100644 --- a/packages/twenty-cli/src/utils/app-manifest-loader.ts +++ b/packages/twenty-cli/src/utils/app-manifest-loader.ts @@ -6,8 +6,8 @@ import { CoreEntityManifest, PackageJson, } from '../types/config.types'; -import { parseJsoncFile } from './jsonc-parser'; import { validateSchema } from '../utils/schema-validator'; +import { parseJsoncFile } from './jsonc-parser'; type Sources = { [key: string]: string | Sources }; diff --git a/packages/twenty-cli/tsconfig.e2e.json b/packages/twenty-cli/tsconfig.e2e.json new file mode 100644 index 00000000000..054545417ed --- /dev/null +++ b/packages/twenty-cli/tsconfig.e2e.json @@ -0,0 +1,31 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist-e2e", + "rootDir": "./src", + "module": "commonjs", + "target": "ES2022", + "moduleResolution": "node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": false, + "declarationMap": false, + "sourceMap": true, + "types": ["jest", "node"] + }, + "include": [ + "src/**/*", + "src/**/__tests__/**/*.e2e-spec.ts" + ], + "exclude": [ + "node_modules", + "dist", + "dist-e2e", + "**/*.test.ts", + "**/*.spec.ts" + ] +} diff --git a/packages/twenty-cli/tsconfig.json b/packages/twenty-cli/tsconfig.json index 18450eba6f9..e48d23f545c 100644 --- a/packages/twenty-cli/tsconfig.json +++ b/packages/twenty-cli/tsconfig.json @@ -1,21 +1,8 @@ { - "extends": "../../tsconfig.base.json", + "extends": "./tsconfig.lib.json", "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src", - "module": "commonjs", - "target": "ES2022", - "moduleResolution": "node", - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true + "composite": true }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"] -} + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts", "**/*.e2e-spec.ts", "**/__tests__/**"] +} \ No newline at end of file diff --git a/packages/twenty-cli/tsconfig.lib.json b/packages/twenty-cli/tsconfig.lib.json new file mode 100644 index 00000000000..f03691f8a42 --- /dev/null +++ b/packages/twenty-cli/tsconfig.lib.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "module": "commonjs", + "target": "ES2022", + "moduleResolution": "node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts", "**/*.e2e-spec.ts", "**/__tests__/**"] +} diff --git a/packages/twenty-server/project.json b/packages/twenty-server/project.json index 8b80d32c8f0..6a1d3ae77ca 100644 --- a/packages/twenty-server/project.json +++ b/packages/twenty-server/project.json @@ -57,6 +57,13 @@ "command": "NODE_ENV=development && nest start" } }, + "start:ci-if-needed": { + "executor": "nx:run-commands", + "options": { + "cwd": "packages/twenty-server", + "command": "curl -f -s http://localhost:3000/healthz > /dev/null 2>&1 && echo '✅ Server already running' || (echo '🚀 Server not running, starting...' && NODE_ENV=development nohup nest start > server.log 2>&1 &)" + } + }, "start:debug": { "executor": "nx:run-commands", "dependsOn": ["build"], diff --git a/yarn.lock b/yarn.lock index 1d71bbbfbd9..387093de04a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -52806,6 +52806,7 @@ __metadata: lodash.kebabcase: "npm:^4.1.1" tsx: "npm:^4.7.0" typescript: "npm:^5.3.0" + wait-on: "npm:^7.2.0" bin: twenty: dist/cli.js languageName: unknown @@ -55410,7 +55411,7 @@ __metadata: languageName: node linkType: hard -"wait-on@npm:^7.0.0": +"wait-on@npm:^7.0.0, wait-on@npm:^7.2.0": version: 7.2.0 resolution: "wait-on@npm:7.2.0" dependencies: