UNPKG

@pact-toolbox/utils

Version:
1,027 lines (1,004 loc) 52.7 kB
import { LogLevels, createConsola } from "consola"; import Docker from "dockerode"; import * as fs from "fs"; import * as path from "path"; import * as tar from "tar-fs"; import { statSync } from "node:fs"; import { colors } from "consola/utils"; import { access, mkdir, writeFile as writeFile$1 } from "node:fs/promises"; import { dirname } from "pathe"; import { exec } from "node:child_process"; import { promisify } from "node:util"; import { detectPort, detectPort as detectPort$1 } from "detect-port"; import { getPort, getRandomPort } from "get-port-please"; import { exec as exec$1, spawn } from "child_process"; import nodeCrypto from "node:crypto"; import { isCancel, select, spinner, text } from "@clack/prompts"; //#region rolldown:runtime var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __commonJS = (cb, mod) => function() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) { key = keys[i]; if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: ((k) => from[k]).bind(null, key), enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod)); //#endregion //#region src/logger.ts const logger = createConsola({ level: 4, formatOptions: { columns: 80, colors: false, compact: false, date: false } }); //#endregion //#region src/chainwebApi.ts /** * Custom error class for Chainweb-related errors. */ var ChainWebError = class extends Error { constructor(message, cause) { super(message); this.cause = cause; this.name = "ChainWebError"; if (cause) this.stack += "\nCaused by: " + cause.stack; } }; /** * Checks if the Chainweb node is healthy. * @param serviceUrl - The base URL of the Chainweb service. * @param timeout - Optional timeout in milliseconds. * @returns Promise<boolean> - True if the node is healthy, false otherwise. */ async function isChainWebNodeOk(serviceUrl, timeout = 5e3) { try { const controller = new AbortController(); const id = setTimeout(() => controller.abort(), timeout); const res = await fetch(`${serviceUrl}/health-check`, { signal: controller.signal }); clearTimeout(id); if (!res.ok) return false; const message = await res.text(); if (message.includes("Health check OK.")) return true; else return false; } catch { return false; } } /** * Checks if the Chainweb node has reached the target block height. * @param targetHeight - The target block height. * @param serviceUrl - The base URL of the Chainweb service. * @param timeout - Optional timeout in milliseconds. * @returns Promise<boolean> - True if the node is at or above the target height, false otherwise. */ async function isChainWebAtHeight(targetHeight, serviceUrl, timeout = 5e3) { try { const controller = new AbortController(); const id = setTimeout(() => controller.abort(), timeout); const res = await fetch(`${serviceUrl}/chainweb/0.0/development/cut`, { signal: controller.signal }); clearTimeout(id); if (!res.ok) { logger.error(`Failed to get chainweb cut: ${res.status} ${res.statusText}`); return false; } const data = await res.json(); if (typeof data.height !== "number") { logger.error(`Invalid response: height is not a number`); return false; } const height = data.height; return height >= targetHeight; } catch (e) { if (e.name === "AbortError") logger.error("Chainweb cut request timed out"); else logger.error(`Failed to get chainweb cut: ${e.message}`); return false; } } /** * Requests the Chainweb node to create blocks on specified chains. * @param params - Parameters including count, chainIds, and onDemandUrl. * @returns Promise<any> - The response data from the server. */ async function makeBlocks({ count = 1, chainIds = ["0"], onDemandUrl }) { const body = JSON.stringify(chainIds.reduce((acc, chainId) => ({ ...acc, [chainId]: count }), {})); const res = await fetch(`${onDemandUrl}/make-blocks`, { method: "POST", headers: { "Content-Type": "application/json" }, body }); if (!res.ok) throw new Error(`Failed to make blocks ${res.status} ${res.statusText}`); return res.json(); } /** * Checks if blocks were successfully created. * @param params - Parameters including count, chainIds, and onDemandUrl. * @returns Promise<boolean> - True if blocks were made successfully, false otherwise. */ async function didMakeBlocks(params) { try { await makeBlocks(params); return true; } catch { return false; } } //#endregion //#region ../../node_modules/.pnpm/@oxc-project+runtime@0.72.2/node_modules/@oxc-project/runtime/src/helpers/typeof.js var require_typeof = __commonJS({ "../../node_modules/.pnpm/@oxc-project+runtime@0.72.2/node_modules/@oxc-project/runtime/src/helpers/typeof.js"(exports, module) { function _typeof$2(o) { "@babel/helpers - typeof"; return module.exports = _typeof$2 = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o$1) { return typeof o$1; } : function(o$1) { return o$1 && "function" == typeof Symbol && o$1.constructor === Symbol && o$1 !== Symbol.prototype ? "symbol" : typeof o$1; }, module.exports.__esModule = true, module.exports["default"] = module.exports, _typeof$2(o); } module.exports = _typeof$2, module.exports.__esModule = true, module.exports["default"] = module.exports; } }); //#endregion //#region ../../node_modules/.pnpm/@oxc-project+runtime@0.72.2/node_modules/@oxc-project/runtime/src/helpers/toPrimitive.js var require_toPrimitive = __commonJS({ "../../node_modules/.pnpm/@oxc-project+runtime@0.72.2/node_modules/@oxc-project/runtime/src/helpers/toPrimitive.js"(exports, module) { var _typeof$1 = require_typeof()["default"]; function toPrimitive$1(t, r) { if ("object" != _typeof$1(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof$1(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } module.exports = toPrimitive$1, module.exports.__esModule = true, module.exports["default"] = module.exports; } }); //#endregion //#region ../../node_modules/.pnpm/@oxc-project+runtime@0.72.2/node_modules/@oxc-project/runtime/src/helpers/toPropertyKey.js var require_toPropertyKey = __commonJS({ "../../node_modules/.pnpm/@oxc-project+runtime@0.72.2/node_modules/@oxc-project/runtime/src/helpers/toPropertyKey.js"(exports, module) { var _typeof = require_typeof()["default"]; var toPrimitive = require_toPrimitive(); function toPropertyKey$1(t) { var i = toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } module.exports = toPropertyKey$1, module.exports.__esModule = true, module.exports["default"] = module.exports; } }); //#endregion //#region ../../node_modules/.pnpm/@oxc-project+runtime@0.72.2/node_modules/@oxc-project/runtime/src/helpers/defineProperty.js var require_defineProperty = __commonJS({ "../../node_modules/.pnpm/@oxc-project+runtime@0.72.2/node_modules/@oxc-project/runtime/src/helpers/defineProperty.js"(exports, module) { var toPropertyKey = require_toPropertyKey(); function _defineProperty$2(e, r, t) { return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } module.exports = _defineProperty$2, module.exports.__esModule = true, module.exports["default"] = module.exports; } }); //#endregion //#region src/cleanup.ts var import_defineProperty$1 = __toESM(require_defineProperty(), 1); var CleanupHandler = class { constructor() { (0, import_defineProperty$1.default)(this, "cleanupFunctions", /* @__PURE__ */ new Set()); (0, import_defineProperty$1.default)(this, "cleanupRegistered", false); (0, import_defineProperty$1.default)(this, "isCleaningUp", false); } registerCleanupFunction(cleanupFn) { this.cleanupFunctions.add(cleanupFn); this.registerSignalHandlers(); } registerSignalHandlers() { if (this.cleanupRegistered) return; this.cleanupRegistered = true; const cleanup = async (signal) => { if (this.isCleaningUp) return; this.isCleaningUp = true; logger.info(`Received ${signal}, running cleanup functions...`); for (const cleanupFn of this.cleanupFunctions) try { await cleanupFn(); } catch (err) { logger.error("Error during cleanup:", err); } process.exit(signal === "uncaughtException" || signal === "unhandledRejection" ? 1 : 0); }; const signals = [ "SIGINT", "SIGTERM", "SIGQUIT", "SIGHUP", "exit", "uncaughtException", "unhandledRejection" ]; signals.forEach((signal) => { process.on(signal, async (reasonOrExitCode) => { if (signal === "exit") await cleanup(signal); else if (signal === "uncaughtException" || signal === "unhandledRejection") { logger.error(`${signal}:`, reasonOrExitCode); await cleanup(signal); } else await cleanup(signal); }); }); } }; const cleanupHandler = new CleanupHandler(); function cleanupOnExit(cleanupFn) { cleanupHandler.registerCleanupFunction(cleanupFn); } //#endregion //#region src/date.ts function formatDate(date) { if (typeof date === "string") date = new Date(date); const { locale, timeZone } = Intl.DateTimeFormat().resolvedOptions(); return date.toLocaleDateString(locale, { month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "numeric", second: "numeric", hour12: false, timeZone }); } //#endregion //#region src/docker/utils.ts const DOCKER_SOCKET = process.env.DOCKER_SOCKET || "/var/run/docker.sock"; function isDockerInstalled() { const socket = DOCKER_SOCKET; try { const stats = statSync(socket); return stats.isSocket(); } catch (e) { logger.error(`Docker is not installed or the socket is not accessible: ${e}`); return false; } } const CHALK_SERVICE_COLORS = [ colors.cyan, colors.green, colors.yellow, colors.blue, colors.magenta, colors.red ]; let colorIndex = 0; const serviceChalkColorMap = /* @__PURE__ */ new Map(); function getServiceColor(serviceName) { if (!serviceChalkColorMap.has(serviceName)) { const selectedChalkFunction = CHALK_SERVICE_COLORS[colorIndex % CHALK_SERVICE_COLORS.length]; serviceChalkColorMap.set(serviceName, selectedChalkFunction); colorIndex++; } return serviceChalkColorMap.get(serviceName); } //#endregion //#region ../../node_modules/.pnpm/@oxc-project+runtime@0.72.2/node_modules/@oxc-project/runtime/src/helpers/checkPrivateRedeclaration.js var require_checkPrivateRedeclaration = __commonJS({ "../../node_modules/.pnpm/@oxc-project+runtime@0.72.2/node_modules/@oxc-project/runtime/src/helpers/checkPrivateRedeclaration.js"(exports, module) { function _checkPrivateRedeclaration(e, t) { if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object"); } module.exports = _checkPrivateRedeclaration, module.exports.__esModule = true, module.exports["default"] = module.exports; } }); //#endregion //#region ../../node_modules/.pnpm/@oxc-project+runtime@0.72.2/node_modules/@oxc-project/runtime/src/helpers/classPrivateMethodInitSpec.js var require_classPrivateMethodInitSpec = __commonJS({ "../../node_modules/.pnpm/@oxc-project+runtime@0.72.2/node_modules/@oxc-project/runtime/src/helpers/classPrivateMethodInitSpec.js"(exports, module) { var checkPrivateRedeclaration$1 = require_checkPrivateRedeclaration(); function _classPrivateMethodInitSpec$2(e, a) { checkPrivateRedeclaration$1(e, a), a.add(e); } module.exports = _classPrivateMethodInitSpec$2, module.exports.__esModule = true, module.exports["default"] = module.exports; } }); //#endregion //#region ../../node_modules/.pnpm/@oxc-project+runtime@0.72.2/node_modules/@oxc-project/runtime/src/helpers/classPrivateFieldInitSpec.js var require_classPrivateFieldInitSpec = __commonJS({ "../../node_modules/.pnpm/@oxc-project+runtime@0.72.2/node_modules/@oxc-project/runtime/src/helpers/classPrivateFieldInitSpec.js"(exports, module) { var checkPrivateRedeclaration = require_checkPrivateRedeclaration(); function _classPrivateFieldInitSpec$2(e, t, a) { checkPrivateRedeclaration(e, t), t.set(e, a); } module.exports = _classPrivateFieldInitSpec$2, module.exports.__esModule = true, module.exports["default"] = module.exports; } }); //#endregion //#region ../../node_modules/.pnpm/@oxc-project+runtime@0.72.2/node_modules/@oxc-project/runtime/src/helpers/assertClassBrand.js var require_assertClassBrand = __commonJS({ "../../node_modules/.pnpm/@oxc-project+runtime@0.72.2/node_modules/@oxc-project/runtime/src/helpers/assertClassBrand.js"(exports, module) { function _assertClassBrand$2(e, t, n) { if ("function" == typeof e ? e === t : e.has(t)) return arguments.length < 3 ? t : n; throw new TypeError("Private element is not present on this object"); } module.exports = _assertClassBrand$2, module.exports.__esModule = true, module.exports["default"] = module.exports; } }); //#endregion //#region ../../node_modules/.pnpm/@oxc-project+runtime@0.72.2/node_modules/@oxc-project/runtime/src/helpers/classPrivateFieldSet2.js var require_classPrivateFieldSet2 = __commonJS({ "../../node_modules/.pnpm/@oxc-project+runtime@0.72.2/node_modules/@oxc-project/runtime/src/helpers/classPrivateFieldSet2.js"(exports, module) { var assertClassBrand$1 = require_assertClassBrand(); function _classPrivateFieldSet2(s, a, r) { return s.set(assertClassBrand$1(s, a), r), r; } module.exports = _classPrivateFieldSet2, module.exports.__esModule = true, module.exports["default"] = module.exports; } }); //#endregion //#region ../../node_modules/.pnpm/@oxc-project+runtime@0.72.2/node_modules/@oxc-project/runtime/src/helpers/classPrivateFieldGet2.js var require_classPrivateFieldGet2 = __commonJS({ "../../node_modules/.pnpm/@oxc-project+runtime@0.72.2/node_modules/@oxc-project/runtime/src/helpers/classPrivateFieldGet2.js"(exports, module) { var assertClassBrand = require_assertClassBrand(); function _classPrivateFieldGet2(s, a) { return s.get(assertClassBrand(s, a)); } module.exports = _classPrivateFieldGet2, module.exports.__esModule = true, module.exports["default"] = module.exports; } }); //#endregion //#region src/docker/DockerService.ts var import_classPrivateMethodInitSpec$1 = __toESM(require_classPrivateMethodInitSpec(), 1); var import_defineProperty = __toESM(require_defineProperty(), 1); var import_classPrivateFieldInitSpec$1 = __toESM(require_classPrivateFieldInitSpec(), 1); var import_classPrivateFieldSet2$1 = __toESM(require_classPrivateFieldSet2(), 1); var import_classPrivateFieldGet2$1 = __toESM(require_classPrivateFieldGet2(), 1); var import_assertClassBrand$1 = __toESM(require_assertClassBrand(), 1); var _docker$1 = /* @__PURE__ */ new WeakMap(); var _networkName$1 = /* @__PURE__ */ new WeakMap(); var _containerId = /* @__PURE__ */ new WeakMap(); var _logStream = /* @__PURE__ */ new WeakMap(); var _coloredPrefix = /* @__PURE__ */ new WeakMap(); var _logger$1 = /* @__PURE__ */ new WeakMap(); var _DockerService_brand = /* @__PURE__ */ new WeakSet(); var DockerService = class { constructor(config, options) { (0, import_classPrivateMethodInitSpec$1.default)(this, _DockerService_brand); (0, import_defineProperty.default)(this, "serviceName", void 0); (0, import_defineProperty.default)(this, "config", void 0); (0, import_defineProperty.default)(this, "containerName", void 0); (0, import_defineProperty.default)(this, "healthCheckFailed", false); (0, import_classPrivateFieldInitSpec$1.default)(this, _docker$1, void 0); (0, import_classPrivateFieldInitSpec$1.default)(this, _networkName$1, void 0); (0, import_classPrivateFieldInitSpec$1.default)(this, _containerId, void 0); (0, import_classPrivateFieldInitSpec$1.default)(this, _logStream, null); (0, import_classPrivateFieldInitSpec$1.default)(this, _coloredPrefix, void 0); (0, import_classPrivateFieldInitSpec$1.default)(this, _logger$1, void 0); this.serviceName = options.serviceName || config.containerName; this.config = config; this.containerName = config.containerName; (0, import_classPrivateFieldSet2$1.default)(_docker$1, this, options.docker); (0, import_classPrivateFieldSet2$1.default)(_networkName$1, this, options.networkName); const colorizer = process.stdout.isTTY ? getServiceColor(this.serviceName) : null; (0, import_classPrivateFieldSet2$1.default)(_coloredPrefix, this, colorizer ? colorizer(this.serviceName) : this.serviceName); (0, import_classPrivateFieldSet2$1.default)(_logger$1, this, options.logger.withTag((0, import_classPrivateFieldGet2$1.default)(_coloredPrefix, this))); } async prepareImage() { if (this.config.build && this.config.image) await (0, import_assertClassBrand$1.default)(_DockerService_brand, this, _buildImage).call(this); else if (this.config.image) await (0, import_assertClassBrand$1.default)(_DockerService_brand, this, _pullImage).call(this); } async start() { (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).start(`Starting service instance...`); await this.prepareImage(); try { const existingContainer = (0, import_classPrivateFieldGet2$1.default)(_docker$1, this).getContainer(this.containerName); const inspectInfo = await existingContainer.inspect(); (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).warn(`Container '${this.containerName}' already exists (State: ${inspectInfo.State.Status}). Attempting to remove it.`); if (inspectInfo.State.Running) await existingContainer.stop({ t: this.config.stopGracePeriod || 10 }).catch((err) => (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).warn(`Could not stop existing container: ${err.message}`)); await existingContainer.remove().catch((err) => (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).warn(`Could not remove existing container: ${err.message}`)); (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).log(`Existing container '${this.containerName}' removed.`); } catch (error) { if (error.statusCode !== 404) { (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).error(`Error checking for existing container:`, error.message || error); throw error; } } let restartPolicy = void 0; const deployRestartPolicy = this.config.deploy?.restartPolicy; const topLevelRestart = this.config.restart; if (deployRestartPolicy) { let dockerodeConditionName = "no"; const composeCondition = deployRestartPolicy.condition; if (composeCondition === "none") dockerodeConditionName = "no"; else if (composeCondition === "on-failure" || composeCondition === "unless-stopped" || composeCondition === "always") dockerodeConditionName = composeCondition; else if (composeCondition) (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).warn(`Unsupported deploy.restart_policy.condition: '${composeCondition}'. Defaulting to 'no'.`); restartPolicy = { Name: dockerodeConditionName, MaximumRetryCount: deployRestartPolicy.maxAttempts }; } else if (topLevelRestart) if (topLevelRestart === "on-failure" || topLevelRestart === "unless-stopped" || topLevelRestart === "always" || topLevelRestart === "no") restartPolicy = { Name: topLevelRestart }; else { (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).warn(`Unsupported top-level restart value: '${topLevelRestart}'. Defaulting to 'no'.`); restartPolicy = { Name: "no" }; } const createOptions = { name: this.containerName, Image: this.config.image, Cmd: this.config.command, Entrypoint: typeof this.config.entrypoint === "string" ? [this.config.entrypoint] : this.config.entrypoint, Env: Array.isArray(this.config.environment) ? this.config.environment : Object.entries(this.config.environment || {}).map(([k, v]) => `${k}=${v}`), ExposedPorts: {}, Labels: this.config.labels, HostConfig: { RestartPolicy: restartPolicy, PortBindings: (0, import_assertClassBrand$1.default)(_DockerService_brand, this, _parsePorts).call(this), Binds: this.config.volumes, NetworkMode: (0, import_classPrivateFieldGet2$1.default)(_networkName$1, this), Ulimits: this.config.ulimits }, NetworkingConfig: { EndpointsConfig: { [(0, import_classPrivateFieldGet2$1.default)(_networkName$1, this)]: {} } }, StopSignal: this.config.stopSignal, StopTimeout: this.config.stopGracePeriod, Healthcheck: this.config.healthCheck, platform: this.config.platform }; if (this.config.expose) this.config.expose.forEach((p) => { createOptions.ExposedPorts[`${p}/tcp`] = {}; }); try { (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).start(`Creating container '${this.containerName}' with image '${this.config.image}'...`); const container = await (0, import_classPrivateFieldGet2$1.default)(_docker$1, this).createContainer(createOptions); (0, import_classPrivateFieldSet2$1.default)(_containerId, this, container.id); (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).log(`Container '${this.containerName}' (ID: ${(0, import_classPrivateFieldGet2$1.default)(_containerId, this)}) created. Starting...`); await container.start(); (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).log(`Service started (Container: ${this.containerName}).`); } catch (error) { (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).error(`Error starting service:`, error.message || error, error.json ? JSON.stringify(error.json) : ""); throw error; } } async stop() { const containerRef = (0, import_classPrivateFieldGet2$1.default)(_containerId, this) || this.containerName; if (!containerRef) { (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).warn(`No container ID or name to stop.`); return; } try { const container = (0, import_classPrivateFieldGet2$1.default)(_docker$1, this).getContainer(containerRef); const inspectInfo = await container.inspect().catch(() => null); if (!inspectInfo) { (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).log(`Container '${containerRef}' not found for stopping.`); return; } (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).log(`Stopping container '${this.containerName}' (ID: ${inspectInfo.Id})...`); await container.stop({ t: this.config.stopGracePeriod || 10 }); (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).log(`Container '${this.containerName}' stopped.`); } catch (error) { if (error.statusCode === 304) (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).log(`Container '${this.containerName}' was already stopped.`); else if (error.statusCode === 404) (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).log(`Container '${containerRef}' not found during stop.`); else (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).warn(`Error stopping container '${this.containerName}':`, error.message || error); } } async remove() { const containerRef = (0, import_classPrivateFieldGet2$1.default)(_containerId, this) || this.containerName; if (!containerRef) { (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).warn(`No container ID or name to remove.`); return; } try { const container = (0, import_classPrivateFieldGet2$1.default)(_docker$1, this).getContainer(containerRef); await container.inspect().catch((err) => { if (err.statusCode === 404) (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).log(`Container '${containerRef}' not found before removal, attempting removal anyway.`); else throw err; }); (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).log(`Removing container '${this.containerName}'...`); await container.remove({ force: true }); (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).log(`Container '${this.containerName}' removed.`); } catch (error) { if (error.statusCode === 404) (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).log(`Container '${containerRef}' was already removed.`); else (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).warn(`Error removing container '${this.containerName}':`, error.message || error); } } async isHealthy() { const containerRef = (0, import_classPrivateFieldGet2$1.default)(_containerId, this) || this.containerName; if (!containerRef) return false; try { const data = await (0, import_classPrivateFieldGet2$1.default)(_docker$1, this).getContainer(containerRef).inspect(); return data.State.Health?.Status === "healthy"; } catch (error) { if (error.statusCode !== 404) (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).error(`Error checking health for container '${containerRef}':`, error.message || error); return false; } } async waitForHealthy(timeoutMs = 12e4, intervalMs = 1e3) { if (!this.config.healthCheck) { (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).log(`No health check defined. Assuming healthy.`); return; } (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).log(`Waiting for container '${this.containerName}' to become healthy (timeout: ${timeoutMs}ms, interval: ${intervalMs}ms)...`); const startTime = Date.now(); while (Date.now() - startTime < timeoutMs) { try { if (await this.isHealthy()) { (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).log(`Container '${this.containerName}' is healthy.`); this.healthCheckFailed = false; return; } } catch (error) { (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).warn(`Health check attempt failed for '${this.containerName}': ${error.message}`); } await new Promise((resolve) => setTimeout(resolve, intervalMs)); } this.healthCheckFailed = true; throw new Error(`Timeout waiting for container '${this.containerName}' to become healthy.`); } async streamLogs() { const containerRef = (0, import_classPrivateFieldGet2$1.default)(_containerId, this) || this.containerName; if (!containerRef) { (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).warn(`No container ID or name to stream logs from.`); return; } try { const container = (0, import_classPrivateFieldGet2$1.default)(_docker$1, this).getContainer(containerRef); const inspectInfo = await container.inspect().catch(() => null); if (!inspectInfo || !inspectInfo.State.Running) { (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).warn(`Container '${containerRef}' is not running. Cannot stream logs.`); return; } (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).log(`Attaching to logs of container '${this.containerName}'...`); const stream = await container.logs({ follow: true, stdout: true, stderr: true, timestamps: true }); (0, import_classPrivateFieldSet2$1.default)(_logStream, this, stream); (0, import_classPrivateFieldGet2$1.default)(_logStream, this).on("data", (chunk) => { let logLine = chunk.toString("utf8"); const potentiallyPrefixed = /^[^a-zA-Z0-9\s\p{P}]*(?=\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})/u; logLine = logLine.replace(potentiallyPrefixed, ""); logLine = logLine.replace(/[^\x20-\x7E\n\r\t]/g, ""); const trimmedMessage = logLine.trimEnd(); if (trimmedMessage) trimmedMessage.split("\n").forEach((line) => { if (line.trim()) console.log(`${(0, import_classPrivateFieldGet2$1.default)(_coloredPrefix, this)} ${line}`); }); }); (0, import_classPrivateFieldGet2$1.default)(_logStream, this).on("end", () => { (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).log(`Log stream ended for container '${this.containerName}'.`); (0, import_classPrivateFieldSet2$1.default)(_logStream, this, null); }); (0, import_classPrivateFieldGet2$1.default)(_logStream, this).on("error", (err) => { (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).error(`Error in log stream for container '${this.containerName}':`, err); (0, import_classPrivateFieldSet2$1.default)(_logStream, this, null); }); } catch (error) { (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).error(`Error attaching to logs for container '${this.containerName}':`, error.message || error); (0, import_classPrivateFieldSet2$1.default)(_logStream, this, null); } } stopLogStream() { if ((0, import_classPrivateFieldGet2$1.default)(_logStream, this)) { (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).log(`Detaching from logs of container '${this.containerName}'.`); if (typeof (0, import_classPrivateFieldGet2$1.default)(_logStream, this).destroy === "function") (0, import_classPrivateFieldGet2$1.default)(_logStream, this).destroy(); else if (typeof (0, import_classPrivateFieldGet2$1.default)(_logStream, this).end === "function") (0, import_classPrivateFieldGet2$1.default)(_logStream, this).end(); (0, import_classPrivateFieldSet2$1.default)(_logStream, this, null); } } }; async function _pullImage() { if (!this.config.image) return; try { const image = (0, import_classPrivateFieldGet2$1.default)(_docker$1, this).getImage(this.config.image); await image.inspect(); return; } catch (error) { if (error.statusCode !== 404) throw error; } (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).start(`Pulling image '${this.config.image}'...`); try { const stream = await (0, import_classPrivateFieldGet2$1.default)(_docker$1, this).pull(this.config.image, {}); await new Promise((resolve, reject) => { (0, import_classPrivateFieldGet2$1.default)(_docker$1, this).modem.followProgress(stream, (err) => err ? reject(err) : resolve()); }); (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).success(`Image '${this.config.image}' pulled successfully.`); } catch (error) { (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).error(`Error pulling image '${this.config.image}':`, error); throw error; } } async function _buildImage() { if (!this.config.build || !this.config.image) return; (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).start(`Building image '${this.config.image}' from context '${this.config.build.context}'...`); const tarStream = tar.pack(this.config.build.context); const dockerfilePath = path.join(this.config.build.context, this.config.build.dockerfile); if (!fs.existsSync(dockerfilePath)) throw new Error(`Dockerfile not found at ${dockerfilePath}`); try { const buildStream = await (0, import_classPrivateFieldGet2$1.default)(_docker$1, this).buildImage(tarStream, { t: this.config.image, dockerfile: this.config.build.dockerfile, q: false }); await new Promise((resolve, reject) => { (0, import_classPrivateFieldGet2$1.default)(_docker$1, this).modem.followProgress(buildStream, (err, res) => { if (err) return reject(err); if (res && res.length > 0) { const lastMessage = res[res.length - 1]; if (lastMessage?.errorDetail) return reject(new Error(lastMessage.errorDetail.message)); } resolve(); }, (event) => { if (event.stream) process.stdout.write(event.stream); }); }); (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).success(`Image '${this.config.image}' built successfully.`); } catch (error) { (0, import_classPrivateFieldGet2$1.default)(_logger$1, this).error(`Error building image '${this.config.image}':`, error); throw error; } } function _parsePorts() { if (!this.config.ports) return void 0; const portBindings = {}; this.config.ports.forEach((p) => { portBindings[`${p.target}/${p.protocol || "tcp"}`] = [{ HostPort: String(p.published) }]; }); return portBindings; } //#endregion //#region src/docker/ContainerOrchestrator.ts var import_classPrivateMethodInitSpec = __toESM(require_classPrivateMethodInitSpec(), 1); var import_classPrivateFieldInitSpec = __toESM(require_classPrivateFieldInitSpec(), 1); var import_classPrivateFieldSet2 = __toESM(require_classPrivateFieldSet2(), 1); var import_classPrivateFieldGet2 = __toESM(require_classPrivateFieldGet2(), 1); var import_assertClassBrand = __toESM(require_assertClassBrand(), 1); var _docker = /* @__PURE__ */ new WeakMap(); var _networkName = /* @__PURE__ */ new WeakMap(); var _networkId = /* @__PURE__ */ new WeakMap(); var _runningServices = /* @__PURE__ */ new WeakMap(); var _logger = /* @__PURE__ */ new WeakMap(); var _spinner = /* @__PURE__ */ new WeakMap(); var _volumes = /* @__PURE__ */ new WeakMap(); var _ContainerOrchestrator_brand = /* @__PURE__ */ new WeakSet(); var ContainerOrchestrator = class { constructor(options) { (0, import_classPrivateMethodInitSpec.default)(this, _ContainerOrchestrator_brand); (0, import_classPrivateFieldInitSpec.default)(this, _docker, new Docker()); (0, import_classPrivateFieldInitSpec.default)(this, _networkName, void 0); (0, import_classPrivateFieldInitSpec.default)(this, _networkId, void 0); (0, import_classPrivateFieldInitSpec.default)(this, _runningServices, void 0); (0, import_classPrivateFieldInitSpec.default)(this, _logger, void 0); (0, import_classPrivateFieldInitSpec.default)(this, _spinner, void 0); (0, import_classPrivateFieldInitSpec.default)(this, _volumes, void 0); (0, import_classPrivateFieldSet2.default)(_networkName, this, options.networkName); (0, import_classPrivateFieldSet2.default)(_runningServices, this, /* @__PURE__ */ new Map()); (0, import_classPrivateFieldSet2.default)(_logger, this, options.logger.withTag("ContainerOrchestrator")); (0, import_classPrivateFieldSet2.default)(_spinner, this, options.spinner); (0, import_classPrivateFieldSet2.default)(_volumes, this, options.volumes || []); } async startServices(serviceConfigs) { (0, import_classPrivateFieldGet2.default)(_logger, this).start(`Starting services...`); await (0, import_assertClassBrand.default)(_ContainerOrchestrator_brand, this, _getOrCreateNetwork).call(this); await (0, import_assertClassBrand.default)(_ContainerOrchestrator_brand, this, _createVolumes).call(this); const orderedServiceGroupNames = (0, import_assertClassBrand.default)(_ContainerOrchestrator_brand, this, _resolveServiceOrder).call(this, serviceConfigs); (0, import_classPrivateFieldGet2.default)(_logger, this).info(`Service group startup order: ${orderedServiceGroupNames.join(", ")}`); for (const serviceGroupName of orderedServiceGroupNames) { const config = serviceConfigs.find((s) => s.containerName === serviceGroupName); if (!config) { (0, import_classPrivateFieldGet2.default)(_logger, this).warn(`Config for service group '${serviceGroupName}' not found. Skipping.`); continue; } const replicaCount = config.deploy?.replicas || 1; const serviceInstances = []; (0, import_classPrivateFieldGet2.default)(_logger, this).info(`Preparing to start ${replicaCount} instance(s) of service group '${serviceGroupName}'...`); for (let i = 0; i < replicaCount; i++) { const instanceName = replicaCount > 1 ? `${serviceGroupName}-${i + 1}` : serviceGroupName; const instanceConfig = { ...config, containerName: instanceName }; const service = new DockerService(instanceConfig, { serviceName: instanceName, docker: (0, import_classPrivateFieldGet2.default)(_docker, this), networkName: (0, import_classPrivateFieldGet2.default)(_networkName, this), logger: (0, import_classPrivateFieldGet2.default)(_logger, this), spinner: (0, import_classPrivateFieldGet2.default)(_spinner, this) }); if (config.dependsOn) for (const depGroupName of Object.keys(config.dependsOn)) { const depServiceGroupInstances = (0, import_classPrivateFieldGet2.default)(_runningServices, this).get(depGroupName); if (!depServiceGroupInstances || depServiceGroupInstances.length === 0) throw new Error(`Dependency group '${depGroupName}' for '${instanceName}' not started or has no instances.`); if (config.dependsOn[depGroupName]?.condition === "service_healthy") { (0, import_classPrivateFieldGet2.default)(_logger, this).start(`Instance '${instanceName}' waiting for all instances of '${depGroupName}' to be healthy...`); try { await Promise.all(depServiceGroupInstances.map((depInstance) => depInstance.waitForHealthy())); (0, import_classPrivateFieldGet2.default)(_logger, this).success(`All instances of '${depGroupName}' are healthy for '${instanceName}'.`); } catch (healthError) { (0, import_classPrivateFieldGet2.default)(_logger, this).error(`Health check failed for at least one instance of dependency group '${depGroupName}' for '${instanceName}': ${healthError.message}`); throw new Error(`Dependency group '${depGroupName}' for '${instanceName}' failed to become healthy.`); } } } try { (0, import_classPrivateFieldGet2.default)(_logger, this).start(`Starting instance '${instanceName}' of service group '${serviceGroupName}'...`); await service.start(); serviceInstances.push(service); } catch (startError) { (0, import_classPrivateFieldGet2.default)(_logger, this).error(`Failed to start instance '${instanceName}' of service group '${serviceGroupName}': ${startError.message}`); throw startError; } } (0, import_classPrivateFieldGet2.default)(_runningServices, this).set(serviceGroupName, serviceInstances); (0, import_classPrivateFieldGet2.default)(_logger, this).success(`All ${replicaCount} instance(s) of service group '${serviceGroupName}' attempted to start.`); } (0, import_classPrivateFieldGet2.default)(_logger, this).success(`All provided service groups attempted to start.`); } async streamAllLogs() { for (const serviceInstances of (0, import_classPrivateFieldGet2.default)(_runningServices, this).values()) for (const service of serviceInstances) service.streamLogs().catch((err) => { (0, import_classPrivateFieldGet2.default)(_logger, this).error(`Error starting log stream for ${service.serviceName}: ${err.message}`); }); } stopAllLogStreams() { for (const serviceInstances of (0, import_classPrivateFieldGet2.default)(_runningServices, this).values()) for (const service of serviceInstances) service.stopLogStream(); } async stopAllServices() { (0, import_classPrivateFieldGet2.default)(_logger, this).start(`Gracefully shutting down all services...`); this.stopAllLogStreams(); const serviceGroupNamesToStop = Array.from((0, import_classPrivateFieldGet2.default)(_runningServices, this).keys()).reverse(); for (const serviceGroupName of serviceGroupNamesToStop) { const serviceInstances = (0, import_classPrivateFieldGet2.default)(_runningServices, this).get(serviceGroupName); if (serviceInstances) { (0, import_classPrivateFieldGet2.default)(_logger, this).start(`Stopping ${serviceInstances.length} instance(s) of service group '${serviceGroupName}'...`); await Promise.all(serviceInstances.map(async (service) => { await service.stop(); await service.remove(); })); (0, import_classPrivateFieldGet2.default)(_logger, this).success(`All instances of service group '${serviceGroupName}' stopped and removed.`); } } (0, import_classPrivateFieldGet2.default)(_runningServices, this).clear(); if ((0, import_classPrivateFieldGet2.default)(_networkId, this)) { try { const network = (0, import_classPrivateFieldGet2.default)(_docker, this).getNetwork((0, import_classPrivateFieldGet2.default)(_networkId, this)); const netInfo = await network.inspect().catch(() => null); if (netInfo && netInfo.Containers && Object.keys(netInfo.Containers).length > 0) (0, import_classPrivateFieldGet2.default)(_logger, this).warn(`Network '${(0, import_classPrivateFieldGet2.default)(_networkName, this)}' (ID: ${(0, import_classPrivateFieldGet2.default)(_networkId, this)}) still has containers: ${Object.keys(netInfo.Containers).join(", ")}. Manual cleanup may be required.`); else if (netInfo) { (0, import_classPrivateFieldGet2.default)(_logger, this).info(`Removing network '${(0, import_classPrivateFieldGet2.default)(_networkName, this)}' (ID: ${(0, import_classPrivateFieldGet2.default)(_networkId, this)})...`); await network.remove(); (0, import_classPrivateFieldGet2.default)(_logger, this).success(`Network '${(0, import_classPrivateFieldGet2.default)(_networkName, this)}' removed.`); } else (0, import_classPrivateFieldGet2.default)(_logger, this).info(`Network '${(0, import_classPrivateFieldGet2.default)(_networkName, this)}' (ID: ${(0, import_classPrivateFieldGet2.default)(_networkId, this)}) not found, likely already removed.`); } catch (error) { if (error.statusCode === 404) (0, import_classPrivateFieldGet2.default)(_logger, this).info(`Network '${(0, import_classPrivateFieldGet2.default)(_networkName, this)}' (ID: ${(0, import_classPrivateFieldGet2.default)(_networkId, this)}) was already removed.`); else (0, import_classPrivateFieldGet2.default)(_logger, this).warn(`Error removing network '${(0, import_classPrivateFieldGet2.default)(_networkName, this)}':`, error.message || error); } (0, import_classPrivateFieldSet2.default)(_networkId, this, void 0); } (0, import_classPrivateFieldGet2.default)(_logger, this).success(`Service cleanup complete.`); } }; async function _getOrCreateNetwork() { try { const network = (0, import_classPrivateFieldGet2.default)(_docker, this).getNetwork((0, import_classPrivateFieldGet2.default)(_networkName, this)); const inspectInfo = await network.inspect(); (0, import_classPrivateFieldSet2.default)(_networkId, this, inspectInfo.Id); } catch (error) { if (error.statusCode === 404) { (0, import_classPrivateFieldGet2.default)(_logger, this).start(`Creating network '${(0, import_classPrivateFieldGet2.default)(_networkName, this)}'...`); const createdNetwork = await (0, import_classPrivateFieldGet2.default)(_docker, this).createNetwork({ Name: (0, import_classPrivateFieldGet2.default)(_networkName, this), Driver: "bridge" }); (0, import_classPrivateFieldSet2.default)(_networkId, this, createdNetwork.id); (0, import_classPrivateFieldGet2.default)(_logger, this).success(`Network '${(0, import_classPrivateFieldGet2.default)(_networkName, this)}' created (ID: ${(0, import_classPrivateFieldGet2.default)(_networkId, this)}).`); } else { (0, import_classPrivateFieldGet2.default)(_logger, this).error(`Error inspecting/creating network ${(0, import_classPrivateFieldGet2.default)(_networkName, this)}:`, error.message || error); throw error; } } if (!(0, import_classPrivateFieldGet2.default)(_networkId, this)) throw new Error(`Failed to obtain network ID for '${(0, import_classPrivateFieldGet2.default)(_networkName, this)}'`); } async function _createVolumes() { for (const volume of (0, import_classPrivateFieldGet2.default)(_volumes, this)) try { (0, import_classPrivateFieldGet2.default)(_docker, this).getVolume(volume); } catch (error) { if (error.statusCode === 404) await (0, import_classPrivateFieldGet2.default)(_docker, this).createVolume({ Name: volume }); } } function _resolveServiceOrder(services) { const serviceMap = new Map(services.map((s) => [s.containerName, s])); const dependencies = /* @__PURE__ */ new Map(); for (const service of services) { const serviceGroupName = service.containerName; if (!dependencies.has(serviceGroupName)) dependencies.set(serviceGroupName, /* @__PURE__ */ new Set()); if (service.dependsOn) { const deps = dependencies.get(serviceGroupName); Object.keys(service.dependsOn).forEach((depGroupName) => { if (serviceMap.has(depGroupName)) deps.add(depGroupName); else (0, import_classPrivateFieldGet2.default)(_logger, this).warn(`Dependency '${depGroupName}' for service '${serviceGroupName}' not found in defined services. It will be ignored.`); }); } } const sorted = []; const visited = /* @__PURE__ */ new Set(); const visiting = /* @__PURE__ */ new Set(); const visit = (serviceGroupName) => { if (visited.has(serviceGroupName)) return; if (visiting.has(serviceGroupName)) throw new Error(`Circular dependency detected: ${serviceGroupName}`); visiting.add(serviceGroupName); const serviceDeps = dependencies.get(serviceGroupName); if (serviceDeps) for (const dep of serviceDeps) visit(dep); visiting.delete(serviceGroupName); visited.add(serviceGroupName); sorted.push(serviceGroupName); }; for (const service of services) { const serviceGroupName = service.containerName; if (!visited.has(serviceGroupName)) visit(serviceGroupName); } return sorted; } //#endregion //#region src/fs.ts async function ensureDir(dirPath) { if (!await access(dirPath).catch(() => false)) await mkdir(dirPath, { recursive: true }); } async function writeFile(filePath, content) { await ensureDir(dirname(filePath)); await writeFile$1(filePath, content.trim()); } //#endregion //#region src/helpers.ts var TimeoutError = class extends Error { constructor(message = "Operation timed out") { super(message); this.name = "TimeoutError"; } }; var AbortError = class extends Error { constructor(message = "Operation aborted") { super(message); this.name = "AbortError"; } }; function delay(ms, signal) { return new Promise((resolve, reject) => { const timer = setTimeout(() => { signal?.removeEventListener("abort", onAbort); resolve(); }, ms); const onAbort = () => { clearTimeout(timer); reject(new AbortError()); }; if (signal) if (signal.aborted) { clearTimeout(timer); reject(new AbortError()); } else signal.addEventListener("abort", onAbort); }); } /** * Polls a function until it returns true or the timeout is reached */ async function pollFn(fn, options) { const { timeout = 3e3, interval = 100, signal, stopOnError = false } = options; const start = performance.now(); while (performance.now() - start < timeout) { if (signal?.aborted) throw new AbortError(); try { const result = await fn(); if (result) return; } catch (err) { if (stopOnError) throw err; } await delay(interval, signal); } throw new TimeoutError(); } const execAsync = promisify(exec); async function executeCommand(command, options) { return execAsync(command, options); } //#endregion //#region src/pact.ts const PACT_VERSION_REGEX = /(\d+)\.(\d+)(?:\.(\d+))?(-[A-Za-z0-9]+)?/; async function isAnyPactInstalled(match) { const version = await getCurrentPactVersion(); return match ? version?.includes(match) ?? false : !!version; } async function getCurrentPactVersion() { try { const { stdout } = await execAsync("pact --version"); const match = stdout.match(PACT_VERSION_REGEX); if (match) return match[0]; } catch { return void 0; } } async function installPact(version, nightly) { if (nightly) return execAsync("npx pactup install --nightly"); if (version) return execAsync(`npx pactup install ${version}`); return execAsync("npx pactup install --latest"); } //#endregion //#region src/port.ts /** * Gets a series of random network ports with gaps between each. * * @param host - The host for which to get the ports. Defaults to '127.0.0.1'. * @param startGap - The minimum gap between successive ports. Defaults to 10. * @param endGap - The maximum gap between successive ports. Defaults to 100. * @returns An object containing the random ports assigned for public, service, on-demand, stratum, and p2p services. * @throws {Error} If it fails to find a suitable port for any of the services. */ async function getRandomNetworkPorts(host = "127.0.0.1", startGap = 10, endGap = 100) { if (startGap <= 0 || endGap <= 0 || startGap > endGap || endGap > 65535) throw new Error("Invalid port gap values provided."); try { const publicPort = await getPort({ host, random: true, name: "public" }); const service = await getPort({ port: publicPort + startGap, host, portRange: [publicPort + startGap, publicPort + endGap], name: "service" }); const onDemand = await getPort({ port: service + startGap, host, portRange: [service + startGap, service + endGap], name: "onDemand" }); const stratum = await getPort({ port: onDemand + startGap, host, portRange: [onDemand + startGap, onDemand + endGap], name: "stratum" }); const p2p = await getPort({ port: stratum + startGap, host, portRange: [stratum + startGap, stratum + endGap], name: "p2p" }); return { public: publicPort, service, onDemand, stratum, p2p }; } catch (error) { throw new Error(`Failed to get network ports: ${error.message}`); } } async function isPortTaken(port) { let p = await detectPort$1(port); if (p == port) return false; return true; } //#endregion //#region src/process.ts function runBin(bin, args, options = {}) { const { cwd = process.cwd(), silent = false, env = process.env, resolveOnStart = true, resolveIf } = options; return new Promise((resolve, reject) => { const child = spawn(bin, args, { cwd, env }); let resolved = false; const handleStdout = (data) => { const output = data.toString(); if (!silent) console.log(output); if (resolveIf && !resolved && resolveIf(output)) { resolved = true; resolve(child); } }; const handleStderr = (data) => { const errorOutput = data.toString(); logger.error(errorOutput); }; const handleError = (err) => { logger.error("Child