diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 9d84da79d5..ab63d26ef6 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -2,7 +2,7 @@ name: Deploy docs to Pages on: push: - branches: ["main"] + branches: ['main'] # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages permissions: @@ -12,7 +12,7 @@ permissions: # Allow one concurrent deployment concurrency: - group: "pages" + group: 'pages' cancel-in-progress: true jobs: @@ -36,7 +36,7 @@ jobs: - run: ./scripts/dev-init.sh - - run: npm -w packages/web-docs run export + - run: npm -w packages/web-docs run build - name: Generate API client docs run: | @@ -46,7 +46,7 @@ jobs: - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: - path: "./packages/web-docs/build" + path: './packages/web-docs/build' - name: Deploy to GitHub Pages id: deployment diff --git a/.github/workflows/dev-to-main-pr.yml b/.github/workflows/dev-to-main-pr.yml index 256322dec5..d57e75a6f6 100644 --- a/.github/workflows/dev-to-main-pr.yml +++ b/.github/workflows/dev-to-main-pr.yml @@ -8,9 +8,11 @@ on: push: branches: - development + - main jobs: mainPromotion: runs-on: ubuntu-latest + if: github.ref == 'refs/heads/development' steps: - name: Generate token id: generate_token @@ -39,6 +41,7 @@ jobs: updateDevelopment: runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' steps: - name: Generate token id: generate_token diff --git a/package-lock.json b/package-lock.json index da7a0983c5..4725d0ef33 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23161,7 +23161,6 @@ "node_modules/@vitest/expect": { "version": "2.0.5", "dev": true, - "license": "MIT", "dependencies": { "@vitest/spy": "2.0.5", "@vitest/utils": "2.0.5", @@ -23241,7 +23240,6 @@ "node_modules/@vitest/runner": { "version": "2.0.5", "dev": true, - "license": "MIT", "dependencies": { "@vitest/utils": "2.0.5", "pathe": "^1.1.2" @@ -23253,7 +23251,6 @@ "node_modules/@vitest/snapshot": { "version": "2.0.5", "dev": true, - "license": "MIT", "dependencies": { "@vitest/pretty-format": "2.0.5", "magic-string": "^0.30.10", @@ -23266,7 +23263,6 @@ "node_modules/@vitest/spy": { "version": "2.0.5", "dev": true, - "license": "MIT", "dependencies": { "tinyspy": "^3.0.0" }, @@ -23277,7 +23273,6 @@ "node_modules/@vitest/utils": { "version": "2.0.5", "dev": true, - "license": "MIT", "dependencies": { "@vitest/pretty-format": "2.0.5", "estree-walker": "^3.0.3", @@ -29998,14 +29993,15 @@ "dependencies": { "tslib": "^2.4.0" }, - "optionalDependencies": { - "@emotion/is-prop-valid": "^0.8.2" - }, "peerDependencies": { + "@emotion/is-prop-valid": "*", "react": "^18.0.0", "react-dom": "^18.0.0" }, "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, "react": { "optional": true }, @@ -30014,21 +30010,6 @@ } } }, - "node_modules/framer-motion/node_modules/@emotion/is-prop-valid": { - "version": "0.8.8", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@emotion/memoize": "0.7.4" - } - }, - "node_modules/framer-motion/node_modules/@emotion/memoize": { - "version": "0.7.4", - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/fresh": { "version": "0.5.2", "license": "MIT", @@ -43140,7 +43121,6 @@ "node_modules/tinyrainbow": { "version": "1.2.0", "dev": true, - "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -43148,7 +43128,6 @@ "node_modules/tinyspy": { "version": "3.0.0", "dev": true, - "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -44258,27 +44237,28 @@ "license": "MIT", "peer": true, "dependencies": { - "esbuild": "^0.18.10", - "postcss": "^8.4.27", - "rollup": "^3.27.1" + "esbuild": "^0.21.3", + "postcss": "^8.4.41", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": "^18.0.0 || >=20.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" }, "optionalDependencies": { - "fsevents": "~2.3.2" + "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": ">= 14", + "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" @@ -44296,6 +44276,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, @@ -44310,7 +44293,6 @@ "node_modules/vite-node": { "version": "2.0.5", "dev": true, - "license": "MIT", "dependencies": { "cac": "^6.7.14", "debug": "^4.3.5", @@ -44613,10 +44595,80 @@ } } }, + "node_modules/vite/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.2.tgz", + "integrity": "sha512-B90tYAUoLhU22olrafY3JQCFLnT3NglazdwkHyxNDYF/zAxJt5fJUB/yBoWFoIQ7SQj+KLe3iL4BhOMa9fzgpw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.2.tgz", + "integrity": "sha512-7twFizNXudESmC9oneLGIUmoHiiLppz/Xs5uJQ4ShvE6234K0VB1/aJYU3f/4g7PhssLGKBVCC37uRkkOi8wjg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/vite/node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true, + "peer": true + }, + "node_modules/vite/node_modules/rollup": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.2.tgz", + "integrity": "sha512-e3TapAgYf9xjdLvKQCkQTnbTKd4a6jwlpQSJJFokHGaX2IVjoEqkIIhiQfqsi0cdwlOD+tQGuOd5AJkc5RngBw==", + "dev": true, + "peer": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.21.2", + "@rollup/rollup-android-arm64": "4.21.2", + "@rollup/rollup-darwin-arm64": "4.21.2", + "@rollup/rollup-darwin-x64": "4.21.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.21.2", + "@rollup/rollup-linux-arm-musleabihf": "4.21.2", + "@rollup/rollup-linux-arm64-gnu": "4.21.2", + "@rollup/rollup-linux-arm64-musl": "4.21.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.21.2", + "@rollup/rollup-linux-riscv64-gnu": "4.21.2", + "@rollup/rollup-linux-s390x-gnu": "4.21.2", + "@rollup/rollup-linux-x64-gnu": "4.21.2", + "@rollup/rollup-linux-x64-musl": "4.21.2", + "@rollup/rollup-win32-arm64-msvc": "4.21.2", + "@rollup/rollup-win32-ia32-msvc": "4.21.2", + "@rollup/rollup-win32-x64-msvc": "4.21.2", + "fsevents": "~2.3.2" + } + }, "node_modules/vitest": { "version": "2.0.5", "dev": true, - "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.3.0", "@vitest/expect": "2.0.5", diff --git a/packages/app-api/src/controllers/GameServerController.ts b/packages/app-api/src/controllers/GameServerController.ts index 691844ebe7..08b9abde46 100644 --- a/packages/app-api/src/controllers/GameServerController.ts +++ b/packages/app-api/src/controllers/GameServerController.ts @@ -1,4 +1,5 @@ import { + IsBoolean, IsEnum, IsISO8601, IsJSON, @@ -86,6 +87,14 @@ class GameServerSearchInputAllowedFilters { @IsOptional() @IsEnum(GAME_SERVER_TYPE, { each: true }) type!: GAME_SERVER_TYPE[]; + + @IsOptional() + @IsBoolean({ each: true }) + reachable!: boolean[]; + + @IsOptional() + @IsBoolean({ each: true }) + enabled!: boolean[]; } class GameServerSearchInputDTO extends ITakaroQuery { diff --git a/packages/app-api/src/service/GameServerService.ts b/packages/app-api/src/service/GameServerService.ts index 708e5c398d..66abd2b04f 100644 --- a/packages/app-api/src/service/GameServerService.ts +++ b/packages/app-api/src/service/GameServerService.ts @@ -81,6 +81,8 @@ export class GameServerOutputDTO extends TakaroModelDTO { type: GAME_SERVER_TYPE; @IsBoolean() reachable: boolean; + @IsBoolean() + enabled: boolean; } export class GameServerCreateDTO extends TakaroDTO { @@ -106,6 +108,9 @@ export class GameServerUpdateDTO extends TakaroDTO { @IsBoolean() @IsOptional() reachable: boolean; + @IsBoolean() + @IsOptional() + enabled: boolean; } export class ModuleInstallDTO extends TakaroDTO { @@ -362,6 +367,9 @@ export class GameServerService extends TakaroService< async getGame(id: string): Promise { const gameserver = await this.repo.findOne(id, true); + + if (!gameserver.enabled) throw new errors.BadRequestError('Game server is disabled'); + let gameInstance = gameClassCache.get(id); if (gameInstance) { diff --git a/packages/app-api/src/workers/playerSyncWorker.ts b/packages/app-api/src/workers/playerSyncWorker.ts index a3807c998c..480149b1a2 100644 --- a/packages/app-api/src/workers/playerSyncWorker.ts +++ b/packages/app-api/src/workers/playerSyncWorker.ts @@ -46,7 +46,7 @@ export async function processJob(job: Job) { const promises = []; const gameserverService = new GameServerService(domain.id); - const gameServers = await gameserverService.find({}); + const gameServers = await gameserverService.find({ filters: { enabled: [true] } }); promises.push( ...gameServers.results.map(async (gs) => { const reachable = await gameserverService.testReachability(gs.id); diff --git a/packages/app-connector/src/lib/GameServerManager.ts b/packages/app-connector/src/lib/GameServerManager.ts index 0ea76367be..2064f623d6 100644 --- a/packages/app-connector/src/lib/GameServerManager.ts +++ b/packages/app-connector/src/lib/GameServerManager.ts @@ -76,15 +76,18 @@ class GameServerManager { */ private async syncServers() { const isFirstTimeRun = this.emitterMap.size === 0; - const domains = await takaro.domain.domainControllerSearch(); - const enabledDomains = domains.data.data.filter((domain) => domain.state === DomainOutputDTOStateEnum.Active); + const enabledDomains = ( + await takaro.domain.domainControllerSearch({ filters: { state: [DomainOutputDTOStateEnum.Active] } }) + ).data.data; const gameServers: Map = new Map(); const results = await Promise.allSettled( enabledDomains.map(async (domain) => { const client = await getDomainClient(domain.id); - const gameServersRes = await client.gameserver.gameServerControllerSearch(); + const gameServersRes = await client.gameserver.gameServerControllerSearch({ + filters: { enabled: [true], reachable: [true] }, + }); gameServers.set(domain.id, gameServersRes.data.data); }), ); @@ -140,6 +143,11 @@ class GameServerManager { return; } + if (!gameServer.enabled) { + this.log.warn(`GameServer ${gameServerId} is not enabled, skipping...`); + return; + } + if (this.emitterMap.has(gameServer.id)) { this.log.warn(`GameServer ${gameServerId} already connected, stopping the existing one...`); await this.remove(gameServer.id); diff --git a/packages/lib-apiclient/package.json b/packages/lib-apiclient/package.json index 2f4baae974..989caab74c 100644 --- a/packages/lib-apiclient/package.json +++ b/packages/lib-apiclient/package.json @@ -11,7 +11,7 @@ "test": "npm run test:unit --if-present && npm run test:integration --if-present", "test:unit": "echo 'No tests (yet :))'", "test:integration": "mocha --config ../../.mocharc.js src/**/*.integration.test.ts --exit", - "generate": "npx @openapitools/openapi-generator-cli generate -i ${TAKARO_HOST}/openapi.json -g typescript-axios -o ./src/generated/", + "generate": "node ../../scripts/wait-until-healthy.mjs && npx @openapitools/openapi-generator-cli generate -i ${TAKARO_HOST}/openapi.json -g typescript-axios -o ./src/generated/", "postgenerate": "./fix-esm.sh && npm run build" }, "keywords": [], @@ -21,4 +21,4 @@ "devDependencies": { "@openapitools/openapi-generator-cli": "^2.5.2" } -} +} \ No newline at end of file diff --git a/packages/lib-apiclient/src/generated/api.ts b/packages/lib-apiclient/src/generated/api.ts index f49b639505..be26e056e0 100644 --- a/packages/lib-apiclient/src/generated/api.ts +++ b/packages/lib-apiclient/src/generated/api.ts @@ -2566,6 +2566,12 @@ export interface GameServerOutputDTO { * @memberof GameServerOutputDTO */ reachable: boolean; + /** + * + * @type {boolean} + * @memberof GameServerOutputDTO + */ + enabled: boolean; /** * * @type {string} @@ -2638,6 +2644,18 @@ export interface GameServerSearchInputAllowedFilters { * @memberof GameServerSearchInputAllowedFilters */ type?: Array; + /** + * + * @type {Array} + * @memberof GameServerSearchInputAllowedFilters + */ + reachable?: Array; + /** + * + * @type {Array} + * @memberof GameServerSearchInputAllowedFilters + */ + enabled?: Array; } export const GameServerSearchInputAllowedFiltersTypeEnum = { @@ -2845,6 +2863,12 @@ export interface GameServerUpdateDTO { * @memberof GameServerUpdateDTO */ reachable?: boolean; + /** + * + * @type {boolean} + * @memberof GameServerUpdateDTO + */ + enabled?: boolean; } export const GameServerUpdateDTOTypeEnum = { diff --git a/packages/lib-db/src/migrations/sql/20240906125101-gameserver-enable.ts b/packages/lib-db/src/migrations/sql/20240906125101-gameserver-enable.ts new file mode 100644 index 0000000000..061c40b5af --- /dev/null +++ b/packages/lib-db/src/migrations/sql/20240906125101-gameserver-enable.ts @@ -0,0 +1,14 @@ +import ms from 'ms'; +import { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + await knex.schema.alterTable('gameservers', (table) => { + table.boolean('enabled').defaultTo(true); + }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.alterTable('gameservers', (table) => { + table.dropColumn('enabled'); + }); +} diff --git a/packages/lib-http/src/controllers/meta.ts b/packages/lib-http/src/controllers/meta.ts index fb561f2c56..7047843075 100644 --- a/packages/lib-http/src/controllers/meta.ts +++ b/packages/lib-http/src/controllers/meta.ts @@ -54,7 +54,7 @@ export class Meta { { info: { title: `Takaro ${process.env.PACKAGE || 'API'}`, - version: process.env.TAKARO_VERSION || '0.0.0', + version: `${process.env.TAKARO_VERSION} - ${process.env.TAKARO_COMMIT} `, contact: { name: 'Takaro Team', email: 'support@takaro.io', @@ -66,9 +66,9 @@ export class Meta { securitySchemes: { adminAuth: { description: 'Used for system administration, like creating or deleting domains', - type: 'http', - scheme: 'bearer', - bearerFormat: 'JWT', + type: 'apiKey', + in: 'header', + name: 'x-takaro-admin-token', }, domainAuth: { description: 'Used for anything inside a domain. Players, GameServers, etc.', @@ -153,7 +153,7 @@ export class Meta { show-method-in-nav-bar="as-colored-block" show-header="false" - allow-authentication="false" + allow-authentication="true" allow-server-selection="false" schema-style="table" diff --git a/packages/test/src/__snapshots__/GameServerController/Create.json b/packages/test/src/__snapshots__/GameServerController/Create.json index f24fbd8bc7..bcad4b8869 100644 --- a/packages/test/src/__snapshots__/GameServerController/Create.json +++ b/packages/test/src/__snapshots__/GameServerController/Create.json @@ -6,6 +6,7 @@ "createdAt": "2023-12-24T15:22:57.559Z", "updatedAt": "2023-12-24T15:22:57.559Z", "name": "Test gameserver", + "enabled": true, "connectionInfo": { "host": "http://takaro:3002" }, diff --git a/packages/test/src/__snapshots__/GameServerController/Get by ID.json b/packages/test/src/__snapshots__/GameServerController/Get by ID.json index d0d38f1c86..0d4ed132b8 100644 --- a/packages/test/src/__snapshots__/GameServerController/Get by ID.json +++ b/packages/test/src/__snapshots__/GameServerController/Get by ID.json @@ -6,6 +6,7 @@ "createdAt": "2023-12-24T15:22:57.034Z", "updatedAt": "2023-12-24T15:22:57.034Z", "name": "Test gameserver", + "enabled": true, "connectionInfo": { "host": "http://takaro:3002" }, diff --git a/packages/test/src/__snapshots__/GameServerController/Update.json b/packages/test/src/__snapshots__/GameServerController/Update.json index 71fbc8adc6..22c4881ce6 100644 --- a/packages/test/src/__snapshots__/GameServerController/Update.json +++ b/packages/test/src/__snapshots__/GameServerController/Update.json @@ -6,6 +6,7 @@ "createdAt": "2023-12-24T15:22:57.938Z", "updatedAt": "2023-12-24T15:22:58.000Z", "name": "Test gameserver 2", + "enabled": true, "connectionInfo": { "host": "somewhere.else", "port": 9876 diff --git a/packages/web-docs/package.json b/packages/web-docs/package.json index d196aaff00..bf5ae756f2 100644 --- a/packages/web-docs/package.json +++ b/packages/web-docs/package.json @@ -41,4 +41,4 @@ "engines": { "node": ">=18.0" } -} +} \ No newline at end of file diff --git a/packages/web-main/src/components/cards/GameServerCard/index.tsx b/packages/web-main/src/components/cards/GameServerCard/index.tsx index 4f07c9a05d..d5147578bf 100644 --- a/packages/web-main/src/components/cards/GameServerCard/index.tsx +++ b/packages/web-main/src/components/cards/GameServerCard/index.tsx @@ -31,7 +31,13 @@ import { useSocket } from 'hooks/useSocket'; import { playersOnGameServersQueryOptions } from 'queries/pog'; import { useQuery } from '@tanstack/react-query'; -export const GameServerCard: FC = ({ id, name, type, reachable }) => { +const StatusChip: FC<{ reachable: boolean; enabled: boolean }> = ({ reachable, enabled }) => { + if (!enabled) return ; + if (!reachable) return ; + return 'online'; +}; + +export const GameServerCard: FC = ({ id, name, type, reachable, enabled }) => { const [openDeleteDialog, setOpenDeleteDialog] = useState(false); const [valid, setValid] = useState(false); const navigate = useNavigate(); @@ -98,7 +104,7 @@ export const GameServerCard: FC = ({ id, name, type, reacha >
- {reachable ? online : } + diff --git a/packages/web-main/src/components/selects/GameServerSelect/GameServerSelect.stories.tsx b/packages/web-main/src/components/selects/GameServerSelect/GameServerSelect.stories.tsx index ed909494ac..aadb6e3d48 100644 --- a/packages/web-main/src/components/selects/GameServerSelect/GameServerSelect.stories.tsx +++ b/packages/web-main/src/components/selects/GameServerSelect/GameServerSelect.stories.tsx @@ -24,6 +24,7 @@ export const Default: StoryFn = () => { id: '1', createdAt: '', updatedAt: '', + enabled: true, connectionInfo: {}, }, { @@ -33,6 +34,7 @@ export const Default: StoryFn = () => { id: '1', createdAt: '', updatedAt: '', + enabled: true, connectionInfo: {}, }, ]; diff --git a/packages/web-main/src/components/selects/ItemSelectQuery/ItemSelectQuery.stories.tsx b/packages/web-main/src/components/selects/ItemSelectQuery/ItemSelectQuery.stories.tsx index a4fdd6e863..0ded7a21bc 100644 --- a/packages/web-main/src/components/selects/ItemSelectQuery/ItemSelectQuery.stories.tsx +++ b/packages/web-main/src/components/selects/ItemSelectQuery/ItemSelectQuery.stories.tsx @@ -27,6 +27,7 @@ export const Default: StoryFn = (args) => { name: 'Online Mock Server 1', type: 'MOCK', reachable: true, + enabled: true, connectionInfo: {}, }; diff --git a/packages/web-main/src/routes/_auth/_global/-gameservers/CreateUpdateForm.tsx b/packages/web-main/src/routes/_auth/_global/-gameservers/CreateUpdateForm.tsx index 1bb926c7d3..53e2b9a242 100644 --- a/packages/web-main/src/routes/_auth/_global/-gameservers/CreateUpdateForm.tsx +++ b/packages/web-main/src/routes/_auth/_global/-gameservers/CreateUpdateForm.tsx @@ -1,5 +1,14 @@ import { FC, useEffect, useState } from 'react'; -import { Button, SelectField, TextField, Drawer, CollapseList, FormError, styled } from '@takaro/lib-components'; +import { + Button, + SelectField, + TextField, + Drawer, + CollapseList, + FormError, + styled, + Switch, +} from '@takaro/lib-components'; import { useForm, SubmitHandler } from 'react-hook-form'; import { IFormInputs, validationSchema } from './validationSchema'; import { @@ -53,10 +62,12 @@ export const CreateUpdateForm: FC = ({ initialData, isLoa defaultValues: { name: '', type: '', + enabled: true, }, ...(initialData && { values: { name: initialData.name, + enabled: initialData.enabled, type: initialData.type, connectionInfo: initialData.connectionInfo, }, @@ -102,6 +113,14 @@ export const CreateUpdateForm: FC = ({ initialData, isLoa placeholder="My cool server" required /> + = ({ name, connectionInfo }) => { + const onSubmit: SubmitHandler = ({ name, connectionInfo, enabled }) => { mutate({ gameServerId, gameServerDetails: { name, + enabled, type: gameServer.type, connectionInfo: JSON.stringify(connectionInfo), }, diff --git a/scripts/wait-until-healthy.mjs b/scripts/wait-until-healthy.mjs new file mode 100644 index 0000000000..9f78de26bd --- /dev/null +++ b/scripts/wait-until-healthy.mjs @@ -0,0 +1,16 @@ +import { Client, AdminClient } from '@takaro/apiclient'; +const takaroHost = process.env.TAKARO_HOST; + +const adminClient = new AdminClient({ + url: takaroHost, + auth: { + clientSecret: 'empty', // We'll only call the health check, so we don't need a real secret + }, +}); + +try { + await adminClient.waitUntilHealthy(); +} catch (error) { + console.error('Could not connect to Takaro API, is your config correct?'); + process.exit(1); +}