diff --git a/composer.json b/composer.json index 9b29039..4b54e3c 100644 --- a/composer.json +++ b/composer.json @@ -16,11 +16,19 @@ "symfony/process": "^8.0", "symfony/dotenv": "^8.0" }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, "autoload": { "psr-4": { "Dsdobrzynski\\DockerAppBuildKit\\": "src/" } }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" + } + }, "bin": [ "bin/dapp-bk" ], diff --git a/nodejs/commands/build.js b/nodejs/commands/build.js index cd47eef..e1c8be2 100644 --- a/nodejs/commands/build.js +++ b/nodejs/commands/build.js @@ -1,4 +1,4 @@ -fimport fs from 'fs/promises'; +import fs from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; import chalk from 'chalk'; @@ -18,35 +18,40 @@ export async function findProjectRoot() { const maxLevels = 5; for (let i = 0; i < maxLevels; i++) { + try { + await fs.access(path.join(current, '.env')); + return current; + } catch {} + try { await fs.access(path.join(current, 'build', '.env')); return current; - } catch { - const parent = path.dirname(current); - if (parent === current) break; - current = parent; - } - } + } catch {} - // Check if we're in build directory - try { - await fs.access(path.join(process.cwd(), '.env')); - return path.dirname(process.cwd()); - } catch { - return null; + const parent = path.dirname(current); + if (parent === current) break; + current = parent; } + + return null; } /** * Load environment variables from .env */ export async function loadEnvironment(projectRoot) { - const envFile = path.join(projectRoot, 'build', '.env'); + let envFile = path.join(projectRoot, '.env'); try { await fs.access(envFile); } catch { - console.error(chalk.red(`Error: Environment file not found: ${envFile}`)); + envFile = path.join(projectRoot, 'build', '.env'); + } + + try { + await fs.access(envFile); + } catch { + console.error(chalk.red(`Error: Environment file not found in project root or build/`)); console.log('Please create .env from .env.example'); return null; } @@ -261,7 +266,11 @@ async function buildAppContainer(docker, env, projectRoot, containerName) { { dockerfile: dockerfile, t: containerName, - buildargs: env.APP_BASE_IMAGE ? { BASE_IMAGE: env.APP_BASE_IMAGE } : undefined, + buildargs: { + ...(env.APP_BASE_IMAGE ? { BASE_IMAGE: env.APP_BASE_IMAGE } : {}), + ...(env.DATA_REL_TYPE ? { DATA_REL_TYPE: env.DATA_REL_TYPE } : {}), + ...(env.DATA_NONREL_TYPE ? { DATA_NONREL_TYPE: env.DATA_NONREL_TYPE } : {}), + }, } ); @@ -293,9 +302,9 @@ async function buildAppContainer(docker, env, projectRoot, containerName) { }; // Add volume mounts if specified - if (env.APP_VOLUME_HOST) { + if (env.APP_HOST_VOLUME_PATH) { createOptions.HostConfig.Binds = [ - `${env.APP_VOLUME_HOST}:${env.APP_VOLUME_CONTAINER || '/var/www/html'}:rw`, + `${env.APP_HOST_VOLUME_PATH}:${env.APP_CONTAINER_VOLUME_PATH || '/var/www/html'}:rw`, ]; } @@ -315,19 +324,261 @@ async function buildAppContainer(docker, env, projectRoot, containerName) { * Handle database containers */ async function handleDataContainers(docker, env, projectRoot, rebuild, importData) { - // Simplified implementation if (env.DATA_REL_TYPE) { - console.log(); - console.log(chalk.cyan('Relational database container handling...')); + if (!(await handleRelationalDatabase(docker, env, projectRoot, rebuild))) { + return false; + } } if (env.DATA_NONREL_TYPE) { - console.log(chalk.cyan('Non-relational database container handling...')); + if (!(await handleNonRelationalDatabase(docker, env, projectRoot, rebuild))) { + return false; + } } return true; } +/** + * Handle relational database container + */ +async function handleRelationalDatabase(docker, env, projectRoot, rebuild) { + const containerName = `${env.PROJECT_NAME}-data-rel-container`; + console.log(); + console.log(chalk.cyan(`Relational Database: ${containerName}`)); + + try { + const container = docker.getContainer(containerName); + const info = await container.inspect(); + + if (!rebuild) { + if (info.State.Status === 'running') { + console.log(`Container ${containerName} is already running`); + return true; + } + console.log(`Starting container ${containerName}`); + await container.start(); + return true; + } + + console.log('Removing existing container for rebuild'); + if (info.State.Status === 'running') { + await container.stop(); + } + await container.remove({ v: true }); + } catch (error) { + // Container doesn't exist, continue to build + } + + return await buildDataContainer(docker, env, projectRoot, containerName, 'rel'); +} + +/** + * Handle non-relational database container + */ +async function handleNonRelationalDatabase(docker, env, projectRoot, rebuild) { + const containerName = `${env.PROJECT_NAME}-data-nonrel-container`; + console.log(); + console.log(chalk.cyan(`Non-Relational Database: ${containerName}`)); + + try { + const container = docker.getContainer(containerName); + const info = await container.inspect(); + + if (!rebuild) { + if (info.State.Status === 'running') { + console.log(`Container ${containerName} is already running`); + return true; + } + console.log(`Starting container ${containerName}`); + await container.start(); + return true; + } + + console.log('Removing existing container for rebuild'); + if (info.State.Status === 'running') { + await container.stop(); + } + await container.remove({ v: true }); + } catch (error) { + // Container doesn't exist, continue to build + } + + return await buildDataContainer(docker, env, projectRoot, containerName, 'nonrel'); +} + +/** + * Build and run a data container (rel or nonrel) + */ +async function buildDataContainer(docker, env, projectRoot, containerName, kind) { + const isRel = kind === 'rel'; + const dataType = isRel ? (env.DATA_REL_TYPE || 'postgres') : env.DATA_NONREL_TYPE; + const dockerfileKey = isRel ? 'DATA_REL_DOCKERFILE' : 'DATA_NONREL_DOCKERFILE'; + const dockerfile = env[dockerfileKey] || getDefaultDataDockerfile(dataType); + const dockerfilePath = path.join(projectRoot, dockerfile); + + try { + await fs.access(dockerfilePath); + } catch { + console.error(chalk.red(`Error: Dockerfile not found: ${dockerfilePath}`)); + return false; + } + + console.log(`Building data container from: ${dockerfile}`); + + try { + const imagePrefix = isRel ? 'data-rel' : 'data-nonrel'; + const imageName = `${env.PROJECT_NAME}-${imagePrefix}-image`; + + const stream = await docker.buildImage( + { context: projectRoot, src: ['.'] }, + { dockerfile, t: imageName } + ); + + await new Promise((resolve, reject) => { + docker.modem.followProgress(stream, (err, res) => (err ? reject(err) : resolve(res)), (event) => { + if (event.stream) process.stdout.write(event.stream); + }); + }); + + return await runDataContainer(docker, env, containerName, imageName, kind); + } catch (error) { + console.error(chalk.red(`Error: Failed to build data container: ${error.message}`)); + return false; + } +} + +/** + * Run data container with type-specific configuration + */ +async function runDataContainer(docker, env, containerName, imageName, kind) { + const isRel = kind === 'rel'; + const dataType = isRel ? (env.DATA_REL_TYPE || 'postgres') : env.DATA_NONREL_TYPE; + const hostPortKey = isRel ? 'DATA_REL_HOST_PORT' : 'DATA_NONREL_HOST_PORT'; + const hostPort = env[hostPortKey] || getDefaultDataPort(dataType); + + console.log(`Starting container: ${containerName} on port ${hostPort}`); + + try { + const createOptions = buildDataContainerConfig(env, containerName, imageName, kind); + const container = await docker.createContainer(createOptions); + await container.start(); + console.log(chalk.green('✓ Data container started successfully')); + return true; + } catch (error) { + console.error(chalk.red(`Error: Failed to run data container: ${error.message}`)); + return false; + } +} + +/** + * Build createContainer options for a data container (exported for testing) + */ +export function buildDataContainerConfig(env, containerName, imageName, kind) { + const isRel = kind === 'rel'; + const dataType = isRel ? (env.DATA_REL_TYPE || 'postgres') : env.DATA_NONREL_TYPE; + const networkName = `${env.PROJECT_NAME}-network`; + const hostPortKey = isRel ? 'DATA_REL_HOST_PORT' : 'DATA_NONREL_HOST_PORT'; + const containerPortKey = isRel ? 'DATA_REL_CONTAINER_PORT' : 'DATA_NONREL_CONTAINER_PORT'; + const hostPort = env[hostPortKey] || getDefaultDataPort(dataType); + const containerPort = env[containerPortKey] || getDefaultDataPort(dataType); + + const createOptions = { + name: containerName, + Image: imageName, + Env: buildDataEnvVars(env, dataType, isRel), + HostConfig: { + NetworkMode: networkName, + PortBindings: { + [`${containerPort}/tcp`]: [{ HostPort: hostPort }], + }, + }, + ExposedPorts: { + [`${containerPort}/tcp`]: {}, + }, + }; + + const hostVolKey = isRel ? 'DATA_REL_HOST_VOLUME_PATH' : 'DATA_NONREL_HOST_VOLUME_PATH'; + const containerVolKey = isRel ? 'DATA_REL_CONTAINER_VOLUME_PATH' : 'DATA_NONREL_CONTAINER_VOLUME_PATH'; + if (env[hostVolKey]) { + createOptions.HostConfig.Binds = [ + `${env[hostVolKey]}:${env[containerVolKey] || getDefaultDataVolume(dataType)}:rw`, + ]; + } + + return createOptions; +} + +/** + * Build Env array for a data container (exported for testing) + */ +export function buildDataEnvVars(env, dataType, isRel) { + const prefix = isRel ? 'DATA_REL' : 'DATA_NONREL'; + const dbName = env[`${prefix}_NAME`] || 'appdb'; + const dbUser = env[`${prefix}_USERNAME`] || 'appuser'; + const dbPassword = env[`${prefix}_PASSWORD`] || 'apppass'; + + switch (dataType) { + case 'mysql': + case 'mariadb': + return [ + `MYSQL_DATABASE=${dbName}`, + `MYSQL_USER=${dbUser}`, + `MYSQL_PASSWORD=${dbPassword}`, + `MYSQL_ROOT_PASSWORD=${dbPassword}`, + ]; + case 'mongodb': + return [ + `MONGO_INITDB_DATABASE=${dbName}`, + `MONGO_INITDB_ROOT_USERNAME=${dbUser}`, + `MONGO_INITDB_ROOT_PASSWORD=${dbPassword}`, + ]; + case 'neo4j': + return [`NEO4J_AUTH=${dbUser}/${dbPassword}`]; + case 'postgres': + default: + return [ + `POSTGRES_DB=${dbName}`, + `POSTGRES_USER=${dbUser}`, + `POSTGRES_PASSWORD=${dbPassword}`, + ]; + } +} + +/** + * Get default Dockerfile path for data type (exported for testing) + */ +export function getDefaultDataDockerfile(dataType) { + const dockerfiles = { + 'mysql': 'docker/data-rel/Dockerfile-data-mysql', + 'mariadb': 'docker/data-rel/Dockerfile-data-mariadb', + 'postgres': 'docker/data-rel/Dockerfile-data-postgres', + 'mongodb': 'docker/data-nonrel/Dockerfile-data-mongodb', + 'neo4j': 'docker/data-nonrel/Dockerfile-data-neo4j', + }; + return dockerfiles[dataType] || `docker/data-rel/Dockerfile-data-${dataType}`; +} + +/** + * Get default port for a database type + */ +function getDefaultDataPort(dataType) { + if (dataType === 'mysql' || dataType === 'mariadb') return '3306'; + if (dataType === 'neo4j') return '7687'; + if (dataType === 'mongodb') return '27017'; + return '5432'; +} + +/** + * Get default container volume path for a data type + */ +function getDefaultDataVolume(dataType) { + if (dataType === 'postgres') return '/var/lib/postgresql/data'; + if (dataType === 'mongodb') return '/data/db'; + if (dataType === 'neo4j') return '/data'; + return '/var/lib/mysql'; +} + /** * Get default Dockerfile path for app type */ diff --git a/nodejs/test/build.test.js b/nodejs/test/build.test.js new file mode 100644 index 0000000..4bf9697 --- /dev/null +++ b/nodejs/test/build.test.js @@ -0,0 +1,178 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { + buildDataContainerConfig, + buildDataEnvVars, + getDefaultDataDockerfile, +} from '../commands/build.js'; + +const relEnv = { + PROJECT_NAME: 'testproject', + DATA_REL_NAME: 'mydb', + DATA_REL_USERNAME: 'myuser', + DATA_REL_PASSWORD: 'mypass', +}; + +const nonRelEnv = { + PROJECT_NAME: 'testproject', + DATA_NONREL_NAME: 'mydb', + DATA_NONREL_USERNAME: 'myuser', + DATA_NONREL_PASSWORD: 'mypass', +}; + +// --- Dockerfile selection --- + +describe('getDefaultDataDockerfile', () => { + it('mysql', () => assert.equal(getDefaultDataDockerfile('mysql'), 'docker/data-rel/Dockerfile-data-mysql')); + it('mariadb', () => assert.equal(getDefaultDataDockerfile('mariadb'), 'docker/data-rel/Dockerfile-data-mariadb')); + it('postgres', () => assert.equal(getDefaultDataDockerfile('postgres'), 'docker/data-rel/Dockerfile-data-postgres')); + it('mongodb', () => assert.equal(getDefaultDataDockerfile('mongodb'), 'docker/data-nonrel/Dockerfile-data-mongodb')); + it('neo4j', () => assert.equal(getDefaultDataDockerfile('neo4j'), 'docker/data-nonrel/Dockerfile-data-neo4j')); +}); + +// --- Env vars --- + +describe('buildDataEnvVars - mysql', () => { + it('sets MYSQL_* vars', () => { + const vars = buildDataEnvVars(relEnv, 'mysql', true); + assert.ok(vars.includes('MYSQL_DATABASE=mydb')); + assert.ok(vars.includes('MYSQL_USER=myuser')); + assert.ok(vars.includes('MYSQL_PASSWORD=mypass')); + assert.ok(vars.includes('MYSQL_ROOT_PASSWORD=mypass')); + }); + + it('sets no POSTGRES_* vars', () => { + const vars = buildDataEnvVars(relEnv, 'mysql', true); + assert.ok(!vars.some(v => v.startsWith('POSTGRES_'))); + }); +}); + +describe('buildDataEnvVars - mariadb', () => { + it('uses MYSQL_* vars', () => { + const vars = buildDataEnvVars(relEnv, 'mariadb', true); + assert.ok(vars.includes('MYSQL_DATABASE=mydb')); + assert.ok(vars.includes('MYSQL_ROOT_PASSWORD=mypass')); + }); +}); + +describe('buildDataEnvVars - postgres', () => { + it('sets POSTGRES_* vars', () => { + const vars = buildDataEnvVars(relEnv, 'postgres', true); + assert.ok(vars.includes('POSTGRES_DB=mydb')); + assert.ok(vars.includes('POSTGRES_USER=myuser')); + assert.ok(vars.includes('POSTGRES_PASSWORD=mypass')); + }); + + it('sets no MYSQL_* vars', () => { + const vars = buildDataEnvVars(relEnv, 'postgres', true); + assert.ok(!vars.some(v => v.startsWith('MYSQL_'))); + }); +}); + +describe('buildDataEnvVars - mongodb', () => { + it('sets MONGO_INITDB_* vars', () => { + const vars = buildDataEnvVars(nonRelEnv, 'mongodb', false); + assert.ok(vars.includes('MONGO_INITDB_DATABASE=mydb')); + assert.ok(vars.includes('MONGO_INITDB_ROOT_USERNAME=myuser')); + assert.ok(vars.includes('MONGO_INITDB_ROOT_PASSWORD=mypass')); + }); +}); + +describe('buildDataEnvVars - neo4j', () => { + it('sets NEO4J_AUTH', () => { + const vars = buildDataEnvVars(nonRelEnv, 'neo4j', false); + assert.ok(vars.includes('NEO4J_AUTH=myuser/mypass')); + }); +}); + +describe('buildDataEnvVars - defaults', () => { + it('falls back to appdb/appuser/apppass when credentials missing', () => { + const vars = buildDataEnvVars({ PROJECT_NAME: 'p' }, 'mysql', true); + assert.ok(vars.includes('MYSQL_DATABASE=appdb')); + assert.ok(vars.includes('MYSQL_USER=appuser')); + assert.ok(vars.includes('MYSQL_PASSWORD=apppass')); + }); +}); + +// --- Port defaults --- + +describe('buildDataContainerConfig - port defaults', () => { + it('mysql defaults to 3306', () => { + const cfg = buildDataContainerConfig({ ...relEnv, DATA_REL_TYPE: 'mysql' }, 'c', 'img', 'rel'); + assert.ok('3306/tcp' in cfg.HostConfig.PortBindings); + assert.equal(cfg.HostConfig.PortBindings['3306/tcp'][0].HostPort, '3306'); + }); + + it('mariadb defaults to 3306', () => { + const cfg = buildDataContainerConfig({ ...relEnv, DATA_REL_TYPE: 'mariadb' }, 'c', 'img', 'rel'); + assert.ok('3306/tcp' in cfg.HostConfig.PortBindings); + }); + + it('postgres defaults to 5432', () => { + const cfg = buildDataContainerConfig({ ...relEnv, DATA_REL_TYPE: 'postgres' }, 'c', 'img', 'rel'); + assert.ok('5432/tcp' in cfg.HostConfig.PortBindings); + assert.equal(cfg.HostConfig.PortBindings['5432/tcp'][0].HostPort, '5432'); + }); + + it('honours custom host port from env', () => { + const cfg = buildDataContainerConfig( + { ...relEnv, DATA_REL_TYPE: 'mysql', DATA_REL_HOST_PORT: '13306', DATA_REL_CONTAINER_PORT: '3306' }, + 'c', 'img', 'rel' + ); + assert.equal(cfg.HostConfig.PortBindings['3306/tcp'][0].HostPort, '13306'); + }); +}); + +// --- Volume mounts --- + +describe('buildDataContainerConfig - volumes', () => { + it('adds Binds when host volume configured', () => { + const cfg = buildDataContainerConfig( + { ...relEnv, DATA_REL_TYPE: 'mysql', DATA_REL_HOST_VOLUME_PATH: '/host/data', DATA_REL_CONTAINER_VOLUME_PATH: '/var/lib/mysql' }, + 'c', 'img', 'rel' + ); + assert.ok(Array.isArray(cfg.HostConfig.Binds)); + assert.ok(cfg.HostConfig.Binds[0].includes('/host/data:/var/lib/mysql')); + }); + + it('uses default mysql volume path when container path omitted', () => { + const cfg = buildDataContainerConfig( + { ...relEnv, DATA_REL_TYPE: 'mysql', DATA_REL_HOST_VOLUME_PATH: '/host/data' }, + 'c', 'img', 'rel' + ); + assert.ok(cfg.HostConfig.Binds[0].includes('/host/data:/var/lib/mysql')); + }); + + it('uses default postgres volume path when container path omitted', () => { + const cfg = buildDataContainerConfig( + { ...relEnv, DATA_REL_TYPE: 'postgres', DATA_REL_HOST_VOLUME_PATH: '/host/data' }, + 'c', 'img', 'rel' + ); + assert.ok(cfg.HostConfig.Binds[0].includes('/host/data:/var/lib/postgresql/data')); + }); + + it('omits Binds when no volume configured', () => { + const cfg = buildDataContainerConfig({ ...relEnv, DATA_REL_TYPE: 'mysql' }, 'c', 'img', 'rel'); + assert.equal(cfg.HostConfig.Binds, undefined); + }); +}); + +// --- Container config structure --- + +describe('buildDataContainerConfig - structure', () => { + it('sets name and image', () => { + const cfg = buildDataContainerConfig({ ...relEnv, DATA_REL_TYPE: 'mysql' }, 'my-container', 'my-image', 'rel'); + assert.equal(cfg.name, 'my-container'); + assert.equal(cfg.Image, 'my-image'); + }); + + it('sets project network', () => { + const cfg = buildDataContainerConfig({ ...relEnv, DATA_REL_TYPE: 'mysql' }, 'c', 'img', 'rel'); + assert.equal(cfg.HostConfig.NetworkMode, 'testproject-network'); + }); + + it('exposes container port', () => { + const cfg = buildDataContainerConfig({ ...relEnv, DATA_REL_TYPE: 'mysql' }, 'c', 'img', 'rel'); + assert.ok('3306/tcp' in cfg.ExposedPorts); + }); +}); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..979da7d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1158 @@ +{ + "name": "@dsdobrzynski/dapp-bk", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@dsdobrzynski/dapp-bk", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "chalk": "^5.0.0", + "commander": "^11.0.0", + "dockerode": "^4.0.0", + "dotenv": "^16.0.0", + "inquirer": "^9.0.0" + }, + "bin": { + "dapp-bk-node": "nodejs/bin/dapp-bk-node.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@balena/dockerignore": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", + "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==", + "license": "Apache-2.0" + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", + "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", + "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.3", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buildcheck": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz", + "integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==", + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "license": "MIT" + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/docker-modem": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.7.tgz", + "integrity": "sha512-XJgGhoR/CLpqshm4d3L7rzH6t8NgDFUIIpztYlLHIApeJjMZKYJMz2zxPsYxnejq5h3ELYSw/RBsi3t5h7gNTA==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.1", + "readable-stream": "^3.5.0", + "split-ca": "^1.0.1", + "ssh2": "^1.15.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/dockerode": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.10.tgz", + "integrity": "sha512-8L/P9JynLBiG7/coiA4FlQXegHltRqS0a+KqI44P1zgQh8QLHTg7FKOwhkBgSJwZTeHsq30WRoVFLuwkfK0YFg==", + "license": "Apache-2.0", + "dependencies": { + "@balena/dockerignore": "^1.0.2", + "@grpc/grpc-js": "^1.11.1", + "@grpc/proto-loader": "^0.7.13", + "docker-modem": "^5.0.7", + "protobufjs": "^7.3.2", + "tar-fs": "^2.1.4", + "uuid": "^10.0.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/inquirer": { + "version": "9.3.8", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.3.8.tgz", + "integrity": "sha512-pFGGdaHrmRKMh4WoDDSowddgjT1Vkl90atobmTeSmcPGdYiwikch/m/Ef5wRaiamHejtw0cUUMMerzDUXCci2w==", + "license": "MIT", + "dependencies": { + "@inquirer/external-editor": "^1.0.2", + "@inquirer/figures": "^1.0.3", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "1.0.0", + "ora": "^5.4.1", + "run-async": "^3.0.0", + "rxjs": "^7.8.1", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/nan": { + "version": "2.26.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.26.2.tgz", + "integrity": "sha512-0tTvBTYkt3tdGw22nrAy50x7gpbGCCFH3AFcyS5WiUu7Eu4vWlri1woE6qHBSfy11vksDqkiwjOnlR7WV8G1Hw==", + "license": "MIT", + "optional": true + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/protobufjs": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz", + "integrity": "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/run-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/split-ca": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", + "integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==", + "license": "ISC" + }, + "node_modules/ssh2": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", + "integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==", + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.10", + "nan": "^2.23.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json index 34423a3..4cd6b8f 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "dapp-bk-node": "./nodejs/bin/dapp-bk-node.js" }, "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "node --test nodejs/test/*.test.js" }, "keywords": [ "docker", diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..d7cb9a3 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,16 @@ + + + + + tests + + + + + src + + + diff --git a/pyproject.toml b/pyproject.toml index f273678..430db71 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,11 @@ dependencies = [ "docker>=5.0.0", ] +[project.optional-dependencies] +dev = [ + "pytest>=7.0.0", +] + [project.scripts] dapp-bk-py = "hig_docker_build_kit.cli:main" diff --git a/python/hig_docker_build_kit/cli.py b/python/hig_docker_build_kit/cli.py index 327f9cd..7f65181 100644 --- a/python/hig_docker_build_kit/cli.py +++ b/python/hig_docker_build_kit/cli.py @@ -184,25 +184,25 @@ def find_project_root() -> Optional[Path]: max_levels = 5 for _ in range(max_levels): + if (current / '.env').exists(): + return current if (current / 'build' / '.env').exists(): return current if current.parent == current: break current = current.parent - # Check if we're in build directory - if (Path.cwd() / '.env').exists(): - return Path.cwd().parent - return None def load_environment(project_root: Path) -> Optional[Dict[str, str]]: """Load environment variables from .env""" - env_file = project_root / 'build' / '.env' + env_file = project_root / '.env' + if not env_file.exists(): + env_file = project_root / 'build' / '.env' if not env_file.exists(): - click.secho(f'Error: Environment file not found: {env_file}', fg='red') + click.secho('Error: Environment file not found in project root or build/', fg='red') click.echo('Please create .env from .env.example') return None @@ -303,7 +303,11 @@ def build_app_container(client: docker.DockerClient, env: Dict[str, str], path=str(project_root), dockerfile=dockerfile, tag=container_name, - buildargs={'BASE_IMAGE': env.get('APP_BASE_IMAGE', '')} if env.get('APP_BASE_IMAGE') else None + buildargs={ + **({'BASE_IMAGE': env['APP_BASE_IMAGE']} if env.get('APP_BASE_IMAGE') else {}), + **({'DATA_REL_TYPE': env['DATA_REL_TYPE']} if env.get('DATA_REL_TYPE') else {}), + **({'DATA_NONREL_TYPE': env['DATA_NONREL_TYPE']} if env.get('DATA_NONREL_TYPE') else {}), + } or None ) for log in build_logs: @@ -318,9 +322,9 @@ def build_app_container(client: docker.DockerClient, env: Dict[str, str], ports = {f'{container_port}/tcp': host_port} volumes = {} - if env.get('APP_VOLUME_HOST'): - volumes[env['APP_VOLUME_HOST']] = { - 'bind': env.get('APP_VOLUME_CONTAINER', '/var/www/html'), + if env.get('APP_HOST_VOLUME_PATH'): + volumes[env['APP_HOST_VOLUME_PATH']] = { + 'bind': env.get('APP_CONTAINER_VOLUME_PATH', '/var/www/html'), 'mode': 'rw' } @@ -342,19 +346,224 @@ def build_app_container(client: docker.DockerClient, env: Dict[str, str], return False -def handle_data_containers(client: docker.DockerClient, env: Dict[str, str], +def handle_data_containers(client: docker.DockerClient, env: Dict[str, str], project_root: Path, rebuild: bool, import_data: bool) -> bool: """Handle database containers""" - # Simplified implementation - full version would mirror bash script logic if env.get('DATA_REL_TYPE'): - click.secho('\nRelational database container handling...', fg='cyan') - + if not handle_relational_database(client, env, project_root, rebuild): + return False + if env.get('DATA_NONREL_TYPE'): - click.secho('Non-relational database container handling...', fg='cyan') - + if not handle_non_relational_database(client, env, project_root, rebuild): + return False + return True +def handle_relational_database(client: docker.DockerClient, env: Dict[str, str], + project_root: Path, rebuild: bool) -> bool: + """Handle relational database container""" + container_name = f"{env['PROJECT_NAME']}-data-rel-container" + click.secho(f'\nRelational Database: {container_name}', fg='cyan') + + container = None + exists = False + try: + container = client.containers.get(container_name) + exists = True + except docker.errors.NotFound: + pass + + if exists and not rebuild: + if container.status == 'running': + click.echo(f'Container {container_name} is already running') + return True + click.echo(f'Starting container {container_name}') + container.start() + return True + + if exists and rebuild: + click.echo('Removing existing container for rebuild') + if container.status == 'running': + container.stop() + container.remove(v=True) + + return build_data_container(client, env, project_root, container_name, 'rel') + + +def handle_non_relational_database(client: docker.DockerClient, env: Dict[str, str], + project_root: Path, rebuild: bool) -> bool: + """Handle non-relational database container""" + container_name = f"{env['PROJECT_NAME']}-data-nonrel-container" + click.secho(f'\nNon-Relational Database: {container_name}', fg='cyan') + + container = None + exists = False + try: + container = client.containers.get(container_name) + exists = True + except docker.errors.NotFound: + pass + + if exists and not rebuild: + if container.status == 'running': + click.echo(f'Container {container_name} is already running') + return True + click.echo(f'Starting container {container_name}') + container.start() + return True + + if exists and rebuild: + click.echo('Removing existing container for rebuild') + if container.status == 'running': + container.stop() + container.remove(v=True) + + return build_data_container(client, env, project_root, container_name, 'nonrel') + + +def build_data_container(client: docker.DockerClient, env: Dict[str, str], + project_root: Path, container_name: str, kind: str) -> bool: + """Build and run a data container""" + is_rel = kind == 'rel' + data_type = env.get('DATA_REL_TYPE', 'postgres') if is_rel else env.get('DATA_NONREL_TYPE', '') + dockerfile_key = 'DATA_REL_DOCKERFILE' if is_rel else 'DATA_NONREL_DOCKERFILE' + dockerfile = env.get(dockerfile_key) or get_default_data_dockerfile(data_type) + dockerfile_path = project_root / dockerfile + + if not dockerfile_path.exists(): + click.secho(f'Error: Dockerfile not found: {dockerfile_path}', fg='red') + return False + + click.echo(f'Building data container from: {dockerfile}') + + try: + prefix = 'data-rel' if is_rel else 'data-nonrel' + image_name = f"{env['PROJECT_NAME']}-{prefix}-image" + + image, build_logs = client.images.build( + path=str(project_root), + dockerfile=dockerfile, + tag=image_name, + ) + + for log in build_logs: + if 'stream' in log: + click.echo(log['stream'], nl=False) + + return run_data_container(client, env, container_name, image_name, kind) + + except Exception as e: + click.secho(f'Error: Failed to build data container: {e}', fg='red') + return False + + +def run_data_container(client: docker.DockerClient, env: Dict[str, str], + container_name: str, image_name: str, kind: str) -> bool: + """Run a data container with type-specific configuration""" + is_rel = kind == 'rel' + data_type = env.get('DATA_REL_TYPE', 'postgres') if is_rel else env.get('DATA_NONREL_TYPE', '') + host_port_key = 'DATA_REL_HOST_PORT' if is_rel else 'DATA_NONREL_HOST_PORT' + container_port_key = 'DATA_REL_CONTAINER_PORT' if is_rel else 'DATA_NONREL_CONTAINER_PORT' + host_port = env.get(host_port_key) or get_default_data_port(data_type) + container_port = env.get(container_port_key) or get_default_data_port(data_type) + network_name = f"{env['PROJECT_NAME']}-network" + + click.echo(f'Starting container: {container_name} on port {host_port}') + + try: + env_vars = build_data_env_vars(env, data_type, is_rel) + ports = {f'{container_port}/tcp': host_port} + volumes = {} + + host_vol_key = 'DATA_REL_HOST_VOLUME_PATH' if is_rel else 'DATA_NONREL_HOST_VOLUME_PATH' + container_vol_key = 'DATA_REL_CONTAINER_VOLUME_PATH' if is_rel else 'DATA_NONREL_CONTAINER_VOLUME_PATH' + if env.get(host_vol_key): + volumes[env[host_vol_key]] = { + 'bind': env.get(container_vol_key) or get_default_data_volume(data_type), + 'mode': 'rw', + } + + client.containers.run( + image_name, + name=container_name, + network=network_name, + environment=env_vars, + ports=ports, + volumes=volumes, + detach=True, + ) + + click.secho('✓ Data container started successfully', fg='green') + return True + + except Exception as e: + click.secho(f'Error: Failed to run data container: {e}', fg='red') + return False + + +def build_data_env_vars(env: Dict[str, str], data_type: str, is_rel: bool) -> Dict[str, str]: + """Build environment variables dict for a data container""" + prefix = 'DATA_REL' if is_rel else 'DATA_NONREL' + db_name = env.get(f'{prefix}_NAME', 'appdb') + db_user = env.get(f'{prefix}_USERNAME', 'appuser') + db_password = env.get(f'{prefix}_PASSWORD', 'apppass') + + if data_type in ('mysql', 'mariadb'): + return { + 'MYSQL_DATABASE': db_name, + 'MYSQL_USER': db_user, + 'MYSQL_PASSWORD': db_password, + 'MYSQL_ROOT_PASSWORD': db_password, + } + if data_type == 'mongodb': + return { + 'MONGO_INITDB_DATABASE': db_name, + 'MONGO_INITDB_ROOT_USERNAME': db_user, + 'MONGO_INITDB_ROOT_PASSWORD': db_password, + } + if data_type == 'neo4j': + return {'NEO4J_AUTH': f'{db_user}/{db_password}'} + return { + 'POSTGRES_DB': db_name, + 'POSTGRES_USER': db_user, + 'POSTGRES_PASSWORD': db_password, + } + + +def get_default_data_dockerfile(data_type: str) -> str: + """Get default Dockerfile path for data type""" + dockerfiles = { + 'mysql': 'docker/data-rel/Dockerfile-data-mysql', + 'mariadb': 'docker/data-rel/Dockerfile-data-mariadb', + 'postgres': 'docker/data-rel/Dockerfile-data-postgres', + 'mongodb': 'docker/data-nonrel/Dockerfile-data-mongodb', + 'neo4j': 'docker/data-nonrel/Dockerfile-data-neo4j', + } + return dockerfiles.get(data_type, f'docker/data-rel/Dockerfile-data-{data_type}') + + +def get_default_data_port(data_type: str) -> str: + """Get default port for database type""" + if data_type in ('mysql', 'mariadb'): + return '3306' + if data_type == 'neo4j': + return '7687' + if data_type == 'mongodb': + return '27017' + return '5432' + + +def get_default_data_volume(data_type: str) -> str: + """Get default container volume path for data type""" + volume_paths = { + 'postgres': '/var/lib/postgresql/data', + 'mongodb': '/data/db', + 'neo4j': '/data', + } + return volume_paths.get(data_type, '/var/lib/mysql') + + def install_composer(container) -> bool: """Install Composer in container""" click.echo('Installing Composer...') diff --git a/python/tests/__init__.py b/python/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/tests/test_cli.py b/python/tests/test_cli.py new file mode 100644 index 0000000..ee6872d --- /dev/null +++ b/python/tests/test_cli.py @@ -0,0 +1,150 @@ +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from hig_docker_build_kit.cli import ( + build_data_env_vars, + get_default_data_dockerfile, + get_default_data_port, + get_default_data_volume, +) + +REL_ENV = { + 'DATA_REL_NAME': 'mydb', + 'DATA_REL_USERNAME': 'myuser', + 'DATA_REL_PASSWORD': 'mypass', +} + +NONREL_ENV = { + 'DATA_NONREL_NAME': 'mydb', + 'DATA_NONREL_USERNAME': 'myuser', + 'DATA_NONREL_PASSWORD': 'mypass', +} + + +# --- Dockerfile selection --- + +class TestGetDefaultDataDockerfile: + def test_mysql(self): + assert get_default_data_dockerfile('mysql') == 'docker/data-rel/Dockerfile-data-mysql' + + def test_mariadb(self): + assert get_default_data_dockerfile('mariadb') == 'docker/data-rel/Dockerfile-data-mariadb' + + def test_postgres(self): + assert get_default_data_dockerfile('postgres') == 'docker/data-rel/Dockerfile-data-postgres' + + def test_mongodb(self): + assert get_default_data_dockerfile('mongodb') == 'docker/data-nonrel/Dockerfile-data-mongodb' + + def test_neo4j(self): + assert get_default_data_dockerfile('neo4j') == 'docker/data-nonrel/Dockerfile-data-neo4j' + + +# --- Default ports --- + +class TestGetDefaultDataPort: + def test_mysql_returns_3306(self): + assert get_default_data_port('mysql') == '3306' + + def test_mariadb_returns_3306(self): + assert get_default_data_port('mariadb') == '3306' + + def test_postgres_returns_5432(self): + assert get_default_data_port('postgres') == '5432' + + def test_mongodb_returns_27017(self): + assert get_default_data_port('mongodb') == '27017' + + def test_neo4j_returns_7687(self): + assert get_default_data_port('neo4j') == '7687' + + +# --- Default volume paths --- + +class TestGetDefaultDataVolume: + def test_mysql(self): + assert get_default_data_volume('mysql') == '/var/lib/mysql' + + def test_mariadb(self): + assert get_default_data_volume('mariadb') == '/var/lib/mysql' + + def test_postgres(self): + assert get_default_data_volume('postgres') == '/var/lib/postgresql/data' + + def test_mongodb(self): + assert get_default_data_volume('mongodb') == '/data/db' + + def test_neo4j(self): + assert get_default_data_volume('neo4j') == '/data' + + +# --- Env vars --- + +class TestBuildDataEnvVarsMysql: + def test_sets_mysql_vars(self): + result = build_data_env_vars(REL_ENV, 'mysql', True) + assert result['MYSQL_DATABASE'] == 'mydb' + assert result['MYSQL_USER'] == 'myuser' + assert result['MYSQL_PASSWORD'] == 'mypass' + assert result['MYSQL_ROOT_PASSWORD'] == 'mypass' + + def test_does_not_set_postgres_vars(self): + result = build_data_env_vars(REL_ENV, 'mysql', True) + assert 'POSTGRES_DB' not in result + + def test_root_password_matches_user_password(self): + result = build_data_env_vars(REL_ENV, 'mysql', True) + assert result['MYSQL_ROOT_PASSWORD'] == result['MYSQL_PASSWORD'] + + +class TestBuildDataEnvVarsMariadb: + def test_uses_mysql_vars(self): + result = build_data_env_vars(REL_ENV, 'mariadb', True) + assert result['MYSQL_DATABASE'] == 'mydb' + assert result['MYSQL_ROOT_PASSWORD'] == 'mypass' + + def test_does_not_set_postgres_vars(self): + result = build_data_env_vars(REL_ENV, 'mariadb', True) + assert 'POSTGRES_DB' not in result + + +class TestBuildDataEnvVarsPostgres: + def test_sets_postgres_vars(self): + result = build_data_env_vars(REL_ENV, 'postgres', True) + assert result['POSTGRES_DB'] == 'mydb' + assert result['POSTGRES_USER'] == 'myuser' + assert result['POSTGRES_PASSWORD'] == 'mypass' + + def test_does_not_set_mysql_vars(self): + result = build_data_env_vars(REL_ENV, 'postgres', True) + assert 'MYSQL_DATABASE' not in result + + +class TestBuildDataEnvVarsMongodb: + def test_sets_mongo_vars(self): + result = build_data_env_vars(NONREL_ENV, 'mongodb', False) + assert result['MONGO_INITDB_DATABASE'] == 'mydb' + assert result['MONGO_INITDB_ROOT_USERNAME'] == 'myuser' + assert result['MONGO_INITDB_ROOT_PASSWORD'] == 'mypass' + + +class TestBuildDataEnvVarsNeo4j: + def test_sets_auth_var(self): + result = build_data_env_vars(NONREL_ENV, 'neo4j', False) + assert result['NEO4J_AUTH'] == 'myuser/mypass' + + +class TestBuildDataEnvVarsDefaults: + def test_falls_back_when_credentials_missing(self): + result = build_data_env_vars({}, 'mysql', True) + assert result['MYSQL_DATABASE'] == 'appdb' + assert result['MYSQL_USER'] == 'appuser' + assert result['MYSQL_PASSWORD'] == 'apppass' + + def test_postgres_defaults(self): + result = build_data_env_vars({}, 'postgres', True) + assert result['POSTGRES_DB'] == 'appdb' + assert result['POSTGRES_USER'] == 'appuser' + assert result['POSTGRES_PASSWORD'] == 'apppass' diff --git a/src/Console/Commands/BuildCommand.php b/src/Console/Commands/BuildCommand.php index 2c4645f..4e3dde0 100644 --- a/src/Console/Commands/BuildCommand.php +++ b/src/Console/Commands/BuildCommand.php @@ -476,61 +476,80 @@ private function buildDataContainer(SymfonyStyle $io, string $containerName, str private function runDataContainer(SymfonyStyle $io, string $containerName, string $imageName): bool { - $networkName = $this->env['PROJECT_NAME'] . '-network'; - $hostPort = $this->env['DATA_REL_HOST_PORT'] ?? '5432'; - $containerPort = $this->env['DATA_REL_CONTAINER_PORT'] ?? '5432'; $dataType = $this->env['DATA_REL_TYPE'] ?? 'postgres'; - - // Database credentials + $hostPort = $this->env['DATA_REL_HOST_PORT'] ?? $this->getDefaultDataPort($dataType); + + $runCommand = $this->buildDataRunCommand($containerName, $imageName); + + $io->text("Starting container: $containerName on port $hostPort"); + $process = new Process($runCommand); + $process->setTimeout(60); + $process->run(); + + if (!$process->isSuccessful()) { + $io->error("Failed to start container: " . $process->getErrorOutput()); + return false; + } + + $io->success("Data container started successfully"); + return true; + } + + protected function buildDataRunCommand(string $containerName, string $imageName): array + { + $dataType = $this->env['DATA_REL_TYPE'] ?? 'postgres'; + $networkName = $this->env['PROJECT_NAME'] . '-network'; + $hostPort = $this->env['DATA_REL_HOST_PORT'] ?? $this->getDefaultDataPort($dataType); + $containerPort = $this->env['DATA_REL_CONTAINER_PORT'] ?? $this->getDefaultDataPort($dataType); $dbName = $this->env['DATA_REL_NAME'] ?? 'appdb'; $dbUser = $this->env['DATA_REL_USERNAME'] ?? 'appuser'; $dbPassword = $this->env['DATA_REL_PASSWORD'] ?? 'apppass'; - $runCommand = [ + $command = [ 'docker', 'run', '-d', '--name', $containerName, '--network', $networkName, '-p', "$hostPort:$containerPort", ]; - // Add database-specific environment variables switch ($dataType) { case 'mysql': case 'mariadb': - $runCommand[] = '-e'; - $runCommand[] = "MYSQL_DATABASE=$dbName"; - $runCommand[] = '-e'; - $runCommand[] = "MYSQL_USER=$dbUser"; - $runCommand[] = '-e'; - $runCommand[] = "MYSQL_PASSWORD=$dbPassword"; - $runCommand[] = '-e'; - $runCommand[] = "MYSQL_ROOT_PASSWORD=$dbPassword"; + array_push($command, + '-e', "MYSQL_DATABASE=$dbName", + '-e', "MYSQL_USER=$dbUser", + '-e', "MYSQL_PASSWORD=$dbPassword", + '-e', "MYSQL_ROOT_PASSWORD=$dbPassword" + ); break; case 'postgres': default: - $runCommand[] = '-e'; - $runCommand[] = "POSTGRES_DB=$dbName"; - $runCommand[] = '-e'; - $runCommand[] = "POSTGRES_USER=$dbUser"; - $runCommand[] = '-e'; - $runCommand[] = "POSTGRES_PASSWORD=$dbPassword"; + array_push($command, + '-e', "POSTGRES_DB=$dbName", + '-e', "POSTGRES_USER=$dbUser", + '-e', "POSTGRES_PASSWORD=$dbPassword" + ); break; } - $runCommand[] = $imageName; + if (!empty($this->env['DATA_REL_HOST_VOLUME_PATH'])) { + $defaultContainerVol = ($dataType === 'postgres') ? '/var/lib/postgresql/data' : '/var/lib/mysql'; + $hostVol = $this->env['DATA_REL_HOST_VOLUME_PATH']; + $containerVol = $this->env['DATA_REL_CONTAINER_VOLUME_PATH'] ?? $defaultContainerVol; + array_push($command, '-v', "$hostVol:$containerVol"); + } - $io->text("Starting container: $containerName on port $hostPort"); - $process = new Process($runCommand); - $process->setTimeout(60); - $process->run(); + $command[] = $imageName; - if (!$process->isSuccessful()) { - $io->error("Failed to start container: " . $process->getErrorOutput()); - return false; - } + return $command; + } - $io->success("Data container started successfully"); - return true; + private function getDefaultDataPort(string $dataType): string + { + return match($dataType) { + 'mysql', 'mariadb' => '3306', + default => '5432', + }; } private function getDefaultDataDockerfile(string $dataType): string diff --git a/tests/Console/Commands/BuildCommandTest.php b/tests/Console/Commands/BuildCommandTest.php new file mode 100644 index 0000000..6e5a641 --- /dev/null +++ b/tests/Console/Commands/BuildCommandTest.php @@ -0,0 +1,303 @@ +getProperty('env'); + $envProp->setAccessible(true); + $envProp->setValue($command, $env); + + $rootProp = $ref->getProperty('projectRoot'); + $rootProp->setAccessible(true); + $rootProp->setValue($command, '/project'); + + return $command; + } + + private function callMethod(object $object, string $method, array $args = []): mixed + { + $ref = new \ReflectionClass($object); + $m = $ref->getMethod($method); + $m->setAccessible(true); + return $m->invokeArgs($object, $args); + } + + // --- Dockerfile selection --- + + public function testGetDefaultDataDockerfileMysql(): void + { + $command = $this->makeCommand([]); + $result = $this->callMethod($command, 'getDefaultDataDockerfile', ['mysql']); + $this->assertSame('docker/data-rel/Dockerfile-data-mysql', $result); + } + + public function testGetDefaultDataDockerfileMariadb(): void + { + $command = $this->makeCommand([]); + $result = $this->callMethod($command, 'getDefaultDataDockerfile', ['mariadb']); + $this->assertSame('docker/data-rel/Dockerfile-data-mariadb', $result); + } + + public function testGetDefaultDataDockerfilePostgres(): void + { + $command = $this->makeCommand([]); + $result = $this->callMethod($command, 'getDefaultDataDockerfile', ['postgres']); + $this->assertSame('docker/data-rel/Dockerfile-data-postgres', $result); + } + + // --- MySQL env vars --- + + public function testBuildDataRunCommandMysqlSetsCorrectEnvVars(): void + { + $command = $this->makeCommand([ + 'PROJECT_NAME' => 'testproject', + 'DATA_REL_TYPE' => 'mysql', + 'DATA_REL_NAME' => 'mydb', + 'DATA_REL_USERNAME' => 'myuser', + 'DATA_REL_PASSWORD' => 'mypass', + 'DATA_REL_HOST_PORT' => '3306', + 'DATA_REL_CONTAINER_PORT' => '3306', + ]); + + $result = $this->callMethod($command, 'buildDataRunCommand', [ + 'testproject-data-rel-container', + 'testproject-data-rel-image', + ]); + + $this->assertContains('MYSQL_DATABASE=mydb', $result); + $this->assertContains('MYSQL_USER=myuser', $result); + $this->assertContains('MYSQL_PASSWORD=mypass', $result); + $this->assertContains('MYSQL_ROOT_PASSWORD=mypass', $result); + } + + public function testBuildDataRunCommandMysqlDoesNotSetPostgresEnvVars(): void + { + $command = $this->makeCommand([ + 'PROJECT_NAME' => 'testproject', + 'DATA_REL_TYPE' => 'mysql', + 'DATA_REL_NAME' => 'mydb', + 'DATA_REL_USERNAME' => 'myuser', + 'DATA_REL_PASSWORD' => 'mypass', + ]); + + $result = $this->callMethod($command, 'buildDataRunCommand', [ + 'testproject-data-rel-container', + 'testproject-data-rel-image', + ]); + + $postgresKeys = array_filter($result, fn($v) => str_starts_with((string)$v, 'POSTGRES_')); + $this->assertEmpty($postgresKeys, 'MySQL command must not include POSTGRES_* env vars'); + } + + // --- MariaDB env vars --- + + public function testBuildDataRunCommandMariadbUsesMysqlEnvVars(): void + { + $command = $this->makeCommand([ + 'PROJECT_NAME' => 'testproject', + 'DATA_REL_TYPE' => 'mariadb', + 'DATA_REL_NAME' => 'mydb', + 'DATA_REL_USERNAME' => 'myuser', + 'DATA_REL_PASSWORD' => 'mypass', + ]); + + $result = $this->callMethod($command, 'buildDataRunCommand', [ + 'testproject-data-rel-container', + 'testproject-data-rel-image', + ]); + + $this->assertContains('MYSQL_DATABASE=mydb', $result); + $this->assertContains('MYSQL_USER=myuser', $result); + $this->assertContains('MYSQL_PASSWORD=mypass', $result); + $this->assertContains('MYSQL_ROOT_PASSWORD=mypass', $result); + } + + // --- Postgres env vars --- + + public function testBuildDataRunCommandPostgresSetsCorrectEnvVars(): void + { + $command = $this->makeCommand([ + 'PROJECT_NAME' => 'testproject', + 'DATA_REL_TYPE' => 'postgres', + 'DATA_REL_NAME' => 'mydb', + 'DATA_REL_USERNAME' => 'myuser', + 'DATA_REL_PASSWORD' => 'mypass', + ]); + + $result = $this->callMethod($command, 'buildDataRunCommand', [ + 'testproject-data-rel-container', + 'testproject-data-rel-image', + ]); + + $this->assertContains('POSTGRES_DB=mydb', $result); + $this->assertContains('POSTGRES_USER=myuser', $result); + $this->assertContains('POSTGRES_PASSWORD=mypass', $result); + + $mysqlKeys = array_filter($result, fn($v) => str_starts_with((string)$v, 'MYSQL_')); + $this->assertEmpty($mysqlKeys, 'Postgres command must not include MYSQL_* env vars'); + } + + // --- Default port selection --- + + public function testBuildDataRunCommandMysqlDefaultsToPort3306(): void + { + $command = $this->makeCommand([ + 'PROJECT_NAME' => 'testproject', + 'DATA_REL_TYPE' => 'mysql', + ]); + + $result = $this->callMethod($command, 'buildDataRunCommand', [ + 'testproject-data-rel-container', + 'testproject-data-rel-image', + ]); + + $this->assertContains('3306:3306', $result); + $this->assertNotContains('5432:5432', $result); + } + + public function testBuildDataRunCommandMariadbDefaultsToPort3306(): void + { + $command = $this->makeCommand([ + 'PROJECT_NAME' => 'testproject', + 'DATA_REL_TYPE' => 'mariadb', + ]); + + $result = $this->callMethod($command, 'buildDataRunCommand', [ + 'testproject-data-rel-container', + 'testproject-data-rel-image', + ]); + + $this->assertContains('3306:3306', $result); + } + + public function testBuildDataRunCommandPostgresDefaultsToPort5432(): void + { + $command = $this->makeCommand([ + 'PROJECT_NAME' => 'testproject', + 'DATA_REL_TYPE' => 'postgres', + ]); + + $result = $this->callMethod($command, 'buildDataRunCommand', [ + 'testproject-data-rel-container', + 'testproject-data-rel-image', + ]); + + $this->assertContains('5432:5432', $result); + } + + // --- Volume mounts --- + + public function testBuildDataRunCommandIncludesVolumeWhenConfigured(): void + { + $command = $this->makeCommand([ + 'PROJECT_NAME' => 'testproject', + 'DATA_REL_TYPE' => 'mysql', + 'DATA_REL_HOST_VOLUME_PATH' => '/host/data', + 'DATA_REL_CONTAINER_VOLUME_PATH' => '/var/lib/mysql', + ]); + + $result = $this->callMethod($command, 'buildDataRunCommand', [ + 'testproject-data-rel-container', + 'testproject-data-rel-image', + ]); + + $this->assertContains('-v', $result); + $this->assertContains('/host/data:/var/lib/mysql', $result); + } + + public function testBuildDataRunCommandMysqlUsesDefaultContainerVolumePath(): void + { + $command = $this->makeCommand([ + 'PROJECT_NAME' => 'testproject', + 'DATA_REL_TYPE' => 'mysql', + 'DATA_REL_HOST_VOLUME_PATH' => '/host/data', + ]); + + $result = $this->callMethod($command, 'buildDataRunCommand', [ + 'testproject-data-rel-container', + 'testproject-data-rel-image', + ]); + + $this->assertContains('/host/data:/var/lib/mysql', $result); + } + + public function testBuildDataRunCommandPostgresUsesDefaultContainerVolumePath(): void + { + $command = $this->makeCommand([ + 'PROJECT_NAME' => 'testproject', + 'DATA_REL_TYPE' => 'postgres', + 'DATA_REL_HOST_VOLUME_PATH' => '/host/data', + ]); + + $result = $this->callMethod($command, 'buildDataRunCommand', [ + 'testproject-data-rel-container', + 'testproject-data-rel-image', + ]); + + $this->assertContains('/host/data:/var/lib/postgresql/data', $result); + } + + public function testBuildDataRunCommandOmitsVolumeWhenNotConfigured(): void + { + $command = $this->makeCommand([ + 'PROJECT_NAME' => 'testproject', + 'DATA_REL_TYPE' => 'mysql', + ]); + + $result = $this->callMethod($command, 'buildDataRunCommand', [ + 'testproject-data-rel-container', + 'testproject-data-rel-image', + ]); + + $this->assertNotContains('-v', $result); + } + + // --- Command structure --- + + public function testBuildDataRunCommandImageIsLastElement(): void + { + $command = $this->makeCommand([ + 'PROJECT_NAME' => 'testproject', + 'DATA_REL_TYPE' => 'mysql', + ]); + + $result = $this->callMethod($command, 'buildDataRunCommand', ['my-container', 'my-image']); + + $this->assertSame('my-image', end($result)); + } + + public function testBuildDataRunCommandIncludesContainerName(): void + { + $command = $this->makeCommand([ + 'PROJECT_NAME' => 'testproject', + 'DATA_REL_TYPE' => 'mysql', + ]); + + $result = $this->callMethod($command, 'buildDataRunCommand', ['my-container', 'my-image']); + + $this->assertContains('my-container', $result); + } + + public function testBuildDataRunCommandIncludesDetachedFlag(): void + { + $command = $this->makeCommand([ + 'PROJECT_NAME' => 'testproject', + 'DATA_REL_TYPE' => 'mysql', + ]); + + $result = $this->callMethod($command, 'buildDataRunCommand', ['my-container', 'my-image']); + + $this->assertContains('-d', $result); + $this->assertSame('docker', $result[0]); + $this->assertSame('run', $result[1]); + } +}