UNPKG

@pact-toolbox/utils

Version:
1 lines 72.6 kB
{"version":3,"file":"index.node.cjs","names":["logger: ConsolaInstance","message: string","cause?: Error | undefined","serviceUrl: string","targetHeight: number","e: any","params: MakeBlocksParams","_typeof","o","_typeof","toPrimitive","toPropertyKey","_defineProperty","cleanupFn: CleanupFunction","signal: NodeJS.Signals | \"exit\" | \"uncaughtException\" | \"unhandledRejection\"","signals: (NodeJS.Signals | \"exit\" | \"uncaughtException\" | \"unhandledRejection\")[]","date: Date | string","DOCKER_SOCKET: string","colors","serviceName: string","checkPrivateRedeclaration","_classPrivateMethodInitSpec","_classPrivateFieldInitSpec","_assertClassBrand","assertClassBrand","config: DockerServiceConfig","options: DockerServiceOptions","err: Error","error: any","restartPolicy: Docker.HostRestartPolicy | undefined","dockerodeConditionName: Docker.HostRestartPolicy[\"Name\"]","createOptions: Docker.ContainerCreateOptions","err: any","line: string","err: Error | null","res: any[] | null","event: any","portBindings: Docker.PortMap","options: ContainerOrchestratorOptions","Docker","serviceConfigs: DockerServiceConfig[]","serviceInstances: DockerService[]","healthError: any","startError: any","error: any","services: DockerServiceConfig[]","sorted: string[]","serviceGroupName: string","dirPath: string","filePath: string","content: string","ms: number","signal?: AbortSignal","fn: () => Promise<boolean>","options: PollOptions","execAsync: typeof exec.__promisify__","exec","command: string","options?: ExecOptions","PACT_VERSION_REGEX: RegExp","match?: string","version?: string","nightly?: boolean","host: string","startGap: number","endGap: number","port: number | string","bin: string","args: string[]","options: RunBinOptions","data: Buffer","err: Error","_code: number | null","_signal: NodeJS.Signals | null","name: string","template: string","context: Record<string, any>","key: string","nodeCrypto"],"sources":["../src/logger.ts","../src/chainwebApi.ts","../../../node_modules/.pnpm/@oxc-project+runtime@0.72.2/node_modules/@oxc-project/runtime/src/helpers/typeof.js","../../../node_modules/.pnpm/@oxc-project+runtime@0.72.2/node_modules/@oxc-project/runtime/src/helpers/toPrimitive.js","../../../node_modules/.pnpm/@oxc-project+runtime@0.72.2/node_modules/@oxc-project/runtime/src/helpers/toPropertyKey.js","../../../node_modules/.pnpm/@oxc-project+runtime@0.72.2/node_modules/@oxc-project/runtime/src/helpers/defineProperty.js","../src/cleanup.ts","../src/date.ts","../src/docker/utils.ts","../../../node_modules/.pnpm/@oxc-project+runtime@0.72.2/node_modules/@oxc-project/runtime/src/helpers/checkPrivateRedeclaration.js","../../../node_modules/.pnpm/@oxc-project+runtime@0.72.2/node_modules/@oxc-project/runtime/src/helpers/classPrivateMethodInitSpec.js","../../../node_modules/.pnpm/@oxc-project+runtime@0.72.2/node_modules/@oxc-project/runtime/src/helpers/classPrivateFieldInitSpec.js","../../../node_modules/.pnpm/@oxc-project+runtime@0.72.2/node_modules/@oxc-project/runtime/src/helpers/assertClassBrand.js","../../../node_modules/.pnpm/@oxc-project+runtime@0.72.2/node_modules/@oxc-project/runtime/src/helpers/classPrivateFieldSet2.js","../../../node_modules/.pnpm/@oxc-project+runtime@0.72.2/node_modules/@oxc-project/runtime/src/helpers/classPrivateFieldGet2.js","../src/docker/DockerService.ts","../src/docker/ContainerOrchestrator.ts","../src/fs.ts","../src/helpers.ts","../src/pact.ts","../src/port.ts","../src/process.ts","../src/template.ts","../../../node_modules/.pnpm/uncrypto@0.1.3/node_modules/uncrypto/dist/crypto.node.mjs","../src/uuid.ts"],"sourcesContent":["import { createConsola, type ConsolaInstance } from \"consola\";\n\nexport const logger: ConsolaInstance = createConsola({\n level: 4,\n formatOptions: {\n columns: 80,\n colors: false,\n compact: false,\n date: false,\n },\n});\n\nexport type Logger = ConsolaInstance;\nexport { LogLevels } from \"consola\";\n","import { logger } from \"./logger\";\n\n/**\n * Custom error class for Chainweb-related errors.\n */\nexport class ChainWebError extends Error {\n constructor(\n message: string,\n public cause?: Error | undefined,\n ) {\n super(message);\n this.name = \"ChainWebError\";\n if (cause) {\n this.stack += \"\\nCaused by: \" + cause.stack;\n }\n }\n}\n\n/**\n * Checks if the Chainweb node is healthy.\n * @param serviceUrl - The base URL of the Chainweb service.\n * @param timeout - Optional timeout in milliseconds.\n * @returns Promise<boolean> - True if the node is healthy, false otherwise.\n */\nexport async function isChainWebNodeOk(serviceUrl: string, timeout = 5000): Promise<boolean> {\n try {\n const controller = new AbortController();\n const id = setTimeout(() => controller.abort(), timeout);\n\n const res = await fetch(`${serviceUrl}/health-check`, {\n signal: controller.signal,\n });\n\n clearTimeout(id);\n\n if (!res.ok) {\n return false;\n }\n\n const message = await res.text();\n if (message.includes(\"Health check OK.\")) {\n return true;\n } else {\n return false;\n }\n } catch {\n return false;\n }\n}\n\n/**\n * Checks if the Chainweb node has reached the target block height.\n * @param targetHeight - The target block height.\n * @param serviceUrl - The base URL of the Chainweb service.\n * @param timeout - Optional timeout in milliseconds.\n * @returns Promise<boolean> - True if the node is at or above the target height, false otherwise.\n */\nexport async function isChainWebAtHeight(targetHeight: number, serviceUrl: string, timeout = 5000): Promise<boolean> {\n try {\n const controller = new AbortController();\n const id = setTimeout(() => controller.abort(), timeout);\n\n const res = await fetch(`${serviceUrl}/chainweb/0.0/development/cut`, {\n signal: controller.signal,\n });\n\n clearTimeout(id);\n\n if (!res.ok) {\n logger.error(`Failed to get chainweb cut: ${res.status} ${res.statusText}`);\n return false;\n }\n\n const data = (await res.json()) as { height: number };\n\n if (typeof data.height !== \"number\") {\n logger.error(`Invalid response: height is not a number`);\n return false;\n }\n\n const height = data.height;\n return height >= targetHeight;\n } catch (e: any) {\n if (e.name === \"AbortError\") {\n logger.error(\"Chainweb cut request timed out\");\n } else {\n logger.error(`Failed to get chainweb cut: ${e.message}`);\n }\n return false;\n }\n}\n\nexport interface MakeBlocksParams {\n count?: number;\n chainIds?: string[];\n onDemandUrl: string;\n}\n\n/**\n * Requests the Chainweb node to create blocks on specified chains.\n * @param params - Parameters including count, chainIds, and onDemandUrl.\n * @returns Promise<any> - The response data from the server.\n */\nexport async function makeBlocks({ count = 1, chainIds = [\"0\"], onDemandUrl }: MakeBlocksParams): Promise<any> {\n const body = JSON.stringify(\n chainIds.reduce((acc, chainId) => ({ ...acc, [chainId]: count }), {} as Record<string, number>),\n );\n\n const res = await fetch(`${onDemandUrl}/make-blocks`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body,\n });\n\n if (!res.ok) {\n throw new Error(`Failed to make blocks ${res.status} ${res.statusText}`);\n }\n\n return res.json();\n}\n\n/**\n * Checks if blocks were successfully created.\n * @param params - Parameters including count, chainIds, and onDemandUrl.\n * @returns Promise<boolean> - True if blocks were made successfully, false otherwise.\n */\nexport async function didMakeBlocks(params: MakeBlocksParams): Promise<boolean> {\n try {\n await makeBlocks(params);\n return true;\n } catch {\n return false;\n }\n}\n","function _typeof(o) {\n \"@babel/helpers - typeof\";\n\n return module.exports = _typeof = \"function\" == typeof Symbol && \"symbol\" == typeof Symbol.iterator ? function (o) {\n return typeof o;\n } : function (o) {\n return o && \"function\" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? \"symbol\" : typeof o;\n }, module.exports.__esModule = true, module.exports[\"default\"] = module.exports, _typeof(o);\n}\nmodule.exports = _typeof, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;","var _typeof = require(\"./typeof.js\")[\"default\"];\nfunction toPrimitive(t, r) {\n if (\"object\" != _typeof(t) || !t) return t;\n var e = t[Symbol.toPrimitive];\n if (void 0 !== e) {\n var i = e.call(t, r || \"default\");\n if (\"object\" != _typeof(i)) return i;\n throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n }\n return (\"string\" === r ? String : Number)(t);\n}\nmodule.exports = toPrimitive, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;","var _typeof = require(\"./typeof.js\")[\"default\"];\nvar toPrimitive = require(\"./toPrimitive.js\");\nfunction toPropertyKey(t) {\n var i = toPrimitive(t, \"string\");\n return \"symbol\" == _typeof(i) ? i : i + \"\";\n}\nmodule.exports = toPropertyKey, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;","var toPropertyKey = require(\"./toPropertyKey.js\");\nfunction _defineProperty(e, r, t) {\n return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {\n value: t,\n enumerable: !0,\n configurable: !0,\n writable: !0\n }) : e[r] = t, e;\n}\nmodule.exports = _defineProperty, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;","import { logger } from \"./logger\";\n\ntype CleanupFunction = () => void | Promise<void>;\n\nclass CleanupHandler {\n private cleanupFunctions: Set<CleanupFunction> = new Set();\n private cleanupRegistered = false;\n private isCleaningUp = false;\n\n registerCleanupFunction(cleanupFn: CleanupFunction) {\n this.cleanupFunctions.add(cleanupFn);\n this.registerSignalHandlers();\n }\n\n private registerSignalHandlers() {\n if (this.cleanupRegistered) return;\n this.cleanupRegistered = true;\n\n const cleanup = async (signal: NodeJS.Signals | \"exit\" | \"uncaughtException\" | \"unhandledRejection\") => {\n if (this.isCleaningUp) return; // Prevent re-entry\n this.isCleaningUp = true;\n\n logger.info(`Received ${signal}, running cleanup functions...`);\n\n for (const cleanupFn of this.cleanupFunctions) {\n try {\n await cleanupFn();\n } catch (err) {\n logger.error(\"Error during cleanup:\", err);\n }\n }\n\n process.exit(signal === \"uncaughtException\" || signal === \"unhandledRejection\" ? 1 : 0);\n };\n\n const signals: (NodeJS.Signals | \"exit\" | \"uncaughtException\" | \"unhandledRejection\")[] = [\n \"SIGINT\",\n \"SIGTERM\",\n \"SIGQUIT\",\n \"SIGHUP\",\n \"exit\",\n \"uncaughtException\",\n \"unhandledRejection\",\n ];\n\n signals.forEach((signal) => {\n process.on(signal as any, async (reasonOrExitCode) => {\n if (signal === \"exit\") {\n await cleanup(signal);\n } else if (signal === \"uncaughtException\" || signal === \"unhandledRejection\") {\n logger.error(`${signal}:`, reasonOrExitCode);\n await cleanup(signal);\n } else {\n await cleanup(signal);\n }\n });\n });\n }\n}\n\nconst cleanupHandler = new CleanupHandler();\n\nexport function cleanupOnExit(cleanupFn: CleanupFunction): void {\n cleanupHandler.registerCleanupFunction(cleanupFn);\n}\n","export function formatDate(date: Date | string): string {\n if (typeof date === \"string\") {\n date = new Date(date);\n }\n const { locale, timeZone } = Intl.DateTimeFormat().resolvedOptions();\n return date.toLocaleDateString(locale, {\n month: \"short\",\n day: \"numeric\",\n year: \"numeric\",\n hour: \"numeric\",\n minute: \"numeric\",\n second: \"numeric\",\n hour12: false,\n timeZone,\n });\n}\n","import { statSync } from \"node:fs\";\nimport { colors } from \"consola/utils\";\n\nimport { logger } from \"../logger\";\n\nexport const DOCKER_SOCKET: string = process.env.DOCKER_SOCKET || \"/var/run/docker.sock\";\n\nexport function isDockerInstalled(): boolean {\n const socket = DOCKER_SOCKET;\n try {\n const stats = statSync(socket);\n return stats.isSocket();\n } catch (e) {\n logger.error(`Docker is not installed or the socket is not accessible: ${e}`);\n return false;\n }\n}\n\nconst CHALK_SERVICE_COLORS = [colors.cyan, colors.green, colors.yellow, colors.blue, colors.magenta, colors.red];\n\nlet colorIndex = 0;\nconst serviceChalkColorMap = new Map<string, typeof colors.cyan>();\n\nexport function getServiceColor(serviceName: string): typeof colors.cyan {\n if (!serviceChalkColorMap.has(serviceName)) {\n const selectedChalkFunction = CHALK_SERVICE_COLORS[colorIndex % CHALK_SERVICE_COLORS.length]!;\n serviceChalkColorMap.set(serviceName, selectedChalkFunction);\n colorIndex++;\n }\n return serviceChalkColorMap.get(serviceName)!;\n}\n","function _checkPrivateRedeclaration(e, t) {\n if (t.has(e)) throw new TypeError(\"Cannot initialize the same private elements twice on an object\");\n}\nmodule.exports = _checkPrivateRedeclaration, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;","var checkPrivateRedeclaration = require(\"./checkPrivateRedeclaration.js\");\nfunction _classPrivateMethodInitSpec(e, a) {\n checkPrivateRedeclaration(e, a), a.add(e);\n}\nmodule.exports = _classPrivateMethodInitSpec, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;","var checkPrivateRedeclaration = require(\"./checkPrivateRedeclaration.js\");\nfunction _classPrivateFieldInitSpec(e, t, a) {\n checkPrivateRedeclaration(e, t), t.set(e, a);\n}\nmodule.exports = _classPrivateFieldInitSpec, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;","function _assertClassBrand(e, t, n) {\n if (\"function\" == typeof e ? e === t : e.has(t)) return arguments.length < 3 ? t : n;\n throw new TypeError(\"Private element is not present on this object\");\n}\nmodule.exports = _assertClassBrand, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;","var assertClassBrand = require(\"./assertClassBrand.js\");\nfunction _classPrivateFieldSet2(s, a, r) {\n return s.set(assertClassBrand(s, a), r), r;\n}\nmodule.exports = _classPrivateFieldSet2, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;","var assertClassBrand = require(\"./assertClassBrand.js\");\nfunction _classPrivateFieldGet2(s, a) {\n return s.get(assertClassBrand(s, a));\n}\nmodule.exports = _classPrivateFieldGet2, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;","import Docker from \"dockerode\";\nimport * as fs from \"fs\";\nimport { Duplex } from \"node:stream\";\nimport * as path from \"path\";\nimport * as tar from \"tar-fs\";\nimport { type Logger } from \"../logger\";\nimport { type DockerServiceConfig } from \"./types\";\nimport { getServiceColor } from \"./utils\";\nimport type { Spinner } from \"../prompts\";\n\ninterface DockerServiceOptions {\n serviceName?: string;\n networkName: string;\n docker: Docker;\n logger: Logger;\n spinner: Spinner;\n}\n\nexport class DockerService {\n public readonly serviceName: string;\n public readonly config: DockerServiceConfig;\n public readonly containerName: string;\n public healthCheckFailed: boolean = false;\n #docker: Docker;\n #networkName: string;\n #containerId?: string;\n #logStream: Duplex | null = null;\n #coloredPrefix: string;\n #logger: Logger;\n // #spinner: Spinner;\n\n constructor(config: DockerServiceConfig, options: DockerServiceOptions) {\n this.serviceName = options.serviceName || config.containerName;\n this.config = config;\n this.containerName = config.containerName;\n this.#docker = options.docker;\n this.#networkName = options.networkName;\n const colorizer = process.stdout.isTTY ? getServiceColor(this.serviceName) : null;\n this.#coloredPrefix = colorizer ? colorizer(this.serviceName) : this.serviceName;\n this.#logger = options.logger.withTag(this.#coloredPrefix);\n // this.#spinner = options.spinner;\n }\n\n async #pullImage(): Promise<void> {\n if (!this.config.image) return;\n try {\n const image = this.#docker.getImage(this.config.image);\n await image.inspect();\n return;\n } catch (error: any) {\n if (error.statusCode !== 404) throw error;\n }\n this.#logger.start(`Pulling image '${this.config.image}'...`);\n try {\n const stream = await this.#docker.pull(this.config.image, {});\n await new Promise<void>((resolve, reject) => {\n this.#docker.modem.followProgress(stream, (err: Error | null) => (err ? reject(err) : resolve()));\n });\n this.#logger.success(`Image '${this.config.image}' pulled successfully.`);\n } catch (error) {\n this.#logger.error(`Error pulling image '${this.config.image}':`, error);\n throw error;\n }\n }\n\n async #buildImage(): Promise<void> {\n if (!this.config.build || !this.config.image) return;\n this.#logger.start(`Building image '${this.config.image}' from context '${this.config.build.context}'...`);\n const tarStream = tar.pack(this.config.build.context);\n const dockerfilePath = path.join(this.config.build.context, this.config.build.dockerfile);\n if (!fs.existsSync(dockerfilePath)) {\n throw new Error(`Dockerfile not found at ${dockerfilePath}`);\n }\n try {\n const buildStream = await this.#docker.buildImage(tarStream as any, {\n t: this.config.image,\n dockerfile: this.config.build.dockerfile,\n q: false,\n });\n await new Promise<void>((resolve, reject) => {\n this.#docker.modem.followProgress(\n buildStream,\n (err: Error | null, res: any[] | null) => {\n if (err) return reject(err);\n if (res && res.length > 0) {\n const lastMessage = res[res.length - 1];\n if (lastMessage?.errorDetail) return reject(new Error(lastMessage.errorDetail.message));\n }\n resolve();\n },\n (event: any) => {\n if (event.stream) process.stdout.write(event.stream);\n },\n );\n });\n this.#logger.success(`Image '${this.config.image}' built successfully.`);\n } catch (error) {\n this.#logger.error(`Error building image '${this.config.image}':`, error);\n throw error;\n }\n }\n\n async prepareImage(): Promise<void> {\n if (this.config.build && this.config.image) {\n await this.#buildImage();\n } else if (this.config.image) {\n await this.#pullImage();\n }\n }\n\n #parsePorts(): Docker.PortMap | undefined {\n if (!this.config.ports) return undefined;\n const portBindings: Docker.PortMap = {};\n this.config.ports.forEach((p) => {\n portBindings[`${p.target}/${p.protocol || \"tcp\"}`] = [{ HostPort: String(p.published) }];\n });\n return portBindings;\n }\n\n async start(): Promise<void> {\n this.#logger.start(`Starting service instance...`);\n await this.prepareImage();\n try {\n const existingContainer = this.#docker.getContainer(this.containerName);\n const inspectInfo = await existingContainer.inspect();\n this.#logger.warn(\n `Container '${this.containerName}' already exists (State: ${inspectInfo.State.Status}). Attempting to remove it.`,\n );\n if (inspectInfo.State.Running) {\n await existingContainer\n .stop({ t: this.config.stopGracePeriod || 10 })\n .catch((err: Error) => this.#logger.warn(`Could not stop existing container: ${err.message}`));\n }\n await existingContainer\n .remove()\n .catch((err: Error) => this.#logger.warn(`Could not remove existing container: ${err.message}`));\n this.#logger.log(`Existing container '${this.containerName}' removed.`);\n } catch (error: any) {\n if (error.statusCode !== 404) {\n this.#logger.error(`Error checking for existing container:`, error.message || error);\n throw error;\n }\n }\n\n let restartPolicy: Docker.HostRestartPolicy | undefined = undefined;\n const deployRestartPolicy = this.config.deploy?.restartPolicy;\n const topLevelRestart = this.config.restart;\n\n if (deployRestartPolicy) {\n let dockerodeConditionName: Docker.HostRestartPolicy[\"Name\"] = \"no\"; // Default to 'no'\n const composeCondition = deployRestartPolicy.condition;\n\n if (composeCondition === \"none\") {\n dockerodeConditionName = \"no\";\n } else if (\n composeCondition === \"on-failure\" ||\n composeCondition === \"unless-stopped\" ||\n composeCondition === \"always\"\n ) {\n dockerodeConditionName = composeCondition;\n } else if (composeCondition) {\n this.#logger.warn(`Unsupported deploy.restart_policy.condition: '${composeCondition}'. Defaulting to 'no'.`);\n }\n\n restartPolicy = {\n Name: dockerodeConditionName,\n MaximumRetryCount: deployRestartPolicy.maxAttempts,\n };\n } else if (topLevelRestart) {\n if (\n topLevelRestart === \"on-failure\" ||\n topLevelRestart === \"unless-stopped\" ||\n topLevelRestart === \"always\" ||\n topLevelRestart === \"no\"\n ) {\n restartPolicy = { Name: topLevelRestart };\n } else {\n this.#logger.warn(`Unsupported top-level restart value: '${topLevelRestart}'. Defaulting to 'no'.`);\n restartPolicy = { Name: \"no\" };\n }\n }\n\n const createOptions: Docker.ContainerCreateOptions = {\n name: this.containerName,\n Image: this.config.image!,\n Cmd: this.config.command,\n Entrypoint: typeof this.config.entrypoint === \"string\" ? [this.config.entrypoint] : this.config.entrypoint,\n Env: Array.isArray(this.config.environment)\n ? this.config.environment\n : Object.entries(this.config.environment || {}).map(([k, v]) => `${k}=${v}`),\n ExposedPorts: {},\n Labels: this.config.labels,\n HostConfig: {\n RestartPolicy: restartPolicy,\n PortBindings: this.#parsePorts(),\n Binds: this.config.volumes,\n NetworkMode: this.#networkName,\n Ulimits: this.config.ulimits,\n },\n NetworkingConfig: { EndpointsConfig: { [this.#networkName]: {} } },\n StopSignal: this.config.stopSignal,\n StopTimeout: this.config.stopGracePeriod,\n Healthcheck: this.config.healthCheck,\n platform: this.config.platform,\n };\n if (this.config.expose) {\n this.config.expose.forEach((p) => {\n createOptions.ExposedPorts![`${p}/tcp`] = {};\n });\n }\n try {\n this.#logger.start(`Creating container '${this.containerName}' with image '${this.config.image}'...`);\n const container = await this.#docker.createContainer(createOptions);\n this.#containerId = container.id;\n this.#logger.log(`Container '${this.containerName}' (ID: ${this.#containerId}) created. Starting...`);\n await container.start();\n this.#logger.log(`Service started (Container: ${this.containerName}).`);\n } catch (error: any) {\n this.#logger.error(\n `Error starting service:`,\n error.message || error,\n error.json ? JSON.stringify(error.json) : \"\",\n );\n throw error;\n }\n }\n\n async stop(): Promise<void> {\n const containerRef = this.#containerId || this.containerName;\n if (!containerRef) {\n this.#logger.warn(`No container ID or name to stop.`);\n return;\n }\n try {\n const container = this.#docker.getContainer(containerRef);\n const inspectInfo = await container.inspect().catch(() => null);\n if (!inspectInfo) {\n this.#logger.log(`Container '${containerRef}' not found for stopping.`);\n return;\n }\n this.#logger.log(`Stopping container '${this.containerName}' (ID: ${inspectInfo.Id})...`);\n await container.stop({ t: this.config.stopGracePeriod || 10 });\n this.#logger.log(`Container '${this.containerName}' stopped.`);\n } catch (error: any) {\n if (error.statusCode === 304) {\n this.#logger.log(`Container '${this.containerName}' was already stopped.`);\n } else if (error.statusCode === 404) {\n this.#logger.log(`Container '${containerRef}' not found during stop.`);\n } else {\n this.#logger.warn(`Error stopping container '${this.containerName}':`, error.message || error);\n }\n }\n }\n\n async remove(): Promise<void> {\n const containerRef = this.#containerId || this.containerName;\n if (!containerRef) {\n this.#logger.warn(`No container ID or name to remove.`);\n return;\n }\n try {\n const container = this.#docker.getContainer(containerRef);\n await container.inspect().catch((err: any) => {\n if (err.statusCode === 404)\n this.#logger.log(`Container '${containerRef}' not found before removal, attempting removal anyway.`);\n else throw err;\n });\n this.#logger.log(`Removing container '${this.containerName}'...`);\n await container.remove({ force: true });\n this.#logger.log(`Container '${this.containerName}' removed.`);\n } catch (error: any) {\n if (error.statusCode === 404) {\n this.#logger.log(`Container '${containerRef}' was already removed.`);\n } else {\n this.#logger.warn(`Error removing container '${this.containerName}':`, error.message || error);\n }\n }\n }\n\n async isHealthy(): Promise<boolean> {\n const containerRef = this.#containerId || this.containerName;\n if (!containerRef) {\n return false;\n }\n try {\n const data = await this.#docker.getContainer(containerRef).inspect();\n return data.State.Health?.Status === \"healthy\";\n } catch (error: any) {\n if (error.statusCode !== 404) {\n this.#logger.error(`Error checking health for container '${containerRef}':`, error.message || error);\n }\n return false;\n }\n }\n\n async waitForHealthy(timeoutMs = 120000, intervalMs = 1000): Promise<void> {\n if (!this.config.healthCheck) {\n this.#logger.log(`No health check defined. Assuming healthy.`);\n return;\n }\n this.#logger.log(\n `Waiting for container '${this.containerName}' to become healthy (timeout: ${timeoutMs}ms, interval: ${intervalMs}ms)...`,\n );\n const startTime = Date.now();\n while (Date.now() - startTime < timeoutMs) {\n try {\n if (await this.isHealthy()) {\n this.#logger.log(`Container '${this.containerName}' is healthy.`);\n this.healthCheckFailed = false;\n return;\n }\n } catch (error: any) {\n this.#logger.warn(`Health check attempt failed for '${this.containerName}': ${error.message}`);\n }\n await new Promise((resolve) => setTimeout(resolve, intervalMs));\n }\n this.healthCheckFailed = true;\n throw new Error(`Timeout waiting for container '${this.containerName}' to become healthy.`);\n }\n\n async streamLogs(): Promise<void> {\n const containerRef = this.#containerId || this.containerName;\n if (!containerRef) {\n this.#logger.warn(`No container ID or name to stream logs from.`);\n return;\n }\n\n try {\n const container = this.#docker.getContainer(containerRef);\n const inspectInfo = await container.inspect().catch(() => null);\n if (!inspectInfo || !inspectInfo.State.Running) {\n this.#logger.warn(`Container '${containerRef}' is not running. Cannot stream logs.`);\n return;\n }\n\n this.#logger.log(`Attaching to logs of container '${this.containerName}'...`);\n const stream = await container.logs({\n follow: true,\n stdout: true,\n stderr: true,\n timestamps: true,\n });\n\n this.#logStream = stream as Duplex;\n\n this.#logStream.on(\"data\", (chunk) => {\n let logLine = chunk.toString(\"utf8\");\n const potentiallyPrefixed = /^[^a-zA-Z0-9\\s\\p{P}]*(?=\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2})/u;\n logLine = logLine.replace(potentiallyPrefixed, \"\");\n logLine = logLine.replace(/[^\\x20-\\x7E\\n\\r\\t]/g, \"\");\n const trimmedMessage = logLine.trimEnd();\n\n if (trimmedMessage) {\n trimmedMessage.split(\"\\n\").forEach((line: string) => {\n if (line.trim()) {\n console.log(`${this.#coloredPrefix} ${line}`);\n }\n });\n }\n });\n\n this.#logStream.on(\"end\", () => {\n this.#logger.log(`Log stream ended for container '${this.containerName}'.`);\n this.#logStream = null;\n });\n\n this.#logStream.on(\"error\", (err) => {\n this.#logger.error(`Error in log stream for container '${this.containerName}':`, err);\n this.#logStream = null;\n });\n } catch (error: any) {\n this.#logger.error(`Error attaching to logs for container '${this.containerName}':`, error.message || error);\n this.#logStream = null;\n }\n }\n\n stopLogStream(): void {\n if (this.#logStream) {\n this.#logger.log(`Detaching from logs of container '${this.containerName}'.`);\n if (typeof this.#logStream.destroy === \"function\") {\n this.#logStream.destroy();\n } else if (typeof (this.#logStream as any).end === \"function\") {\n (this.#logStream as any).end();\n }\n this.#logStream = null;\n }\n }\n}\n","import Docker from \"dockerode\";\nimport type { Logger } from \"../logger\";\nimport { DockerService } from \"./DockerService\";\nimport type { DockerServiceConfig } from \"./types\";\nimport type { Spinner } from \"../prompts\";\n\ninterface ContainerOrchestratorOptions {\n networkName: string;\n volumes: string[];\n logger: Logger;\n spinner: Spinner;\n}\n\nexport class ContainerOrchestrator {\n #docker: Docker = new Docker();\n #networkName: string;\n #networkId?: string;\n #runningServices: Map<string, DockerService[]>;\n #logger: Logger;\n #spinner: Spinner;\n #volumes: string[];\n\n constructor(options: ContainerOrchestratorOptions) {\n this.#networkName = options.networkName;\n this.#runningServices = new Map();\n this.#logger = options.logger.withTag(\"ContainerOrchestrator\");\n this.#spinner = options.spinner;\n this.#volumes = options.volumes || [];\n }\n\n async #getOrCreateNetwork(): Promise<void> {\n try {\n const network = this.#docker.getNetwork(this.#networkName);\n const inspectInfo = await network.inspect();\n this.#networkId = inspectInfo.Id;\n } catch (error: any) {\n if (error.statusCode === 404) {\n this.#logger.start(`Creating network '${this.#networkName}'...`);\n const createdNetwork = await this.#docker.createNetwork({\n Name: this.#networkName,\n Driver: \"bridge\",\n });\n this.#networkId = createdNetwork.id;\n this.#logger.success(`Network '${this.#networkName}' created (ID: ${this.#networkId}).`);\n } else {\n this.#logger.error(`Error inspecting/creating network ${this.#networkName}:`, error.message || error);\n throw error;\n }\n }\n if (!this.#networkId) {\n throw new Error(`Failed to obtain network ID for '${this.#networkName}'`);\n }\n }\n\n async #createVolumes(): Promise<void> {\n for (const volume of this.#volumes) {\n try {\n this.#docker.getVolume(volume);\n } catch (error: any) {\n if (error.statusCode === 404) {\n await this.#docker.createVolume({ Name: volume });\n }\n }\n }\n }\n\n #resolveServiceOrder(services: DockerServiceConfig[]): string[] {\n const serviceMap = new Map(services.map((s) => [s.containerName, s]));\n const dependencies = new Map<string, Set<string>>();\n\n for (const service of services) {\n const serviceGroupName = service.containerName!;\n if (!dependencies.has(serviceGroupName)) {\n dependencies.set(serviceGroupName, new Set());\n }\n if (service.dependsOn) {\n const deps = dependencies.get(serviceGroupName)!;\n Object.keys(service.dependsOn).forEach((depGroupName) => {\n if (serviceMap.has(depGroupName)) {\n deps.add(depGroupName);\n } else {\n this.#logger.warn(\n `Dependency '${depGroupName}' for service '${serviceGroupName}' not found in defined services. It will be ignored.`,\n );\n }\n });\n }\n }\n\n const sorted: string[] = [];\n const visited = new Set<string>();\n const visiting = new Set<string>();\n\n const visit = (serviceGroupName: string): void => {\n if (visited.has(serviceGroupName)) return;\n if (visiting.has(serviceGroupName)) {\n throw new Error(`Circular dependency detected: ${serviceGroupName}`);\n }\n visiting.add(serviceGroupName);\n const serviceDeps = dependencies.get(serviceGroupName);\n if (serviceDeps) {\n for (const dep of serviceDeps) {\n visit(dep);\n }\n }\n visiting.delete(serviceGroupName);\n visited.add(serviceGroupName);\n sorted.push(serviceGroupName);\n };\n\n for (const service of services) {\n const serviceGroupName = service.containerName!;\n if (!visited.has(serviceGroupName)) {\n visit(serviceGroupName);\n }\n }\n return sorted;\n }\n\n async startServices(serviceConfigs: DockerServiceConfig[]): Promise<void> {\n this.#logger.start(`Starting services...`);\n await this.#getOrCreateNetwork();\n await this.#createVolumes();\n const orderedServiceGroupNames = this.#resolveServiceOrder(serviceConfigs);\n this.#logger.info(`Service group startup order: ${orderedServiceGroupNames.join(\", \")}`);\n\n for (const serviceGroupName of orderedServiceGroupNames) {\n const config = serviceConfigs.find((s) => s.containerName === serviceGroupName)!;\n if (!config) {\n this.#logger.warn(`Config for service group '${serviceGroupName}' not found. Skipping.`);\n continue;\n }\n\n const replicaCount = config.deploy?.replicas || 1;\n const serviceInstances: DockerService[] = [];\n\n this.#logger.info(`Preparing to start ${replicaCount} instance(s) of service group '${serviceGroupName}'...`);\n\n for (let i = 0; i < replicaCount; i++) {\n const instanceName = replicaCount > 1 ? `${serviceGroupName}-${i + 1}` : serviceGroupName;\n const instanceConfig = { ...config, containerName: instanceName };\n\n const service = new DockerService(instanceConfig, {\n serviceName: instanceName,\n docker: this.#docker,\n networkName: this.#networkName,\n logger: this.#logger,\n spinner: this.#spinner,\n });\n\n if (config.dependsOn) {\n for (const depGroupName of Object.keys(config.dependsOn)) {\n const depServiceGroupInstances = this.#runningServices.get(depGroupName);\n if (!depServiceGroupInstances || depServiceGroupInstances.length === 0) {\n throw new Error(\n `Dependency group '${depGroupName}' for '${instanceName}' not started or has no instances.`,\n );\n }\n if (config.dependsOn[depGroupName]?.condition === \"service_healthy\") {\n this.#logger.start(\n `Instance '${instanceName}' waiting for all instances of '${depGroupName}' to be healthy...`,\n );\n try {\n await Promise.all(depServiceGroupInstances.map((depInstance) => depInstance.waitForHealthy()));\n this.#logger.success(`All instances of '${depGroupName}' are healthy for '${instanceName}'.`);\n } catch (healthError: any) {\n this.#logger.error(\n `Health check failed for at least one instance of dependency group '${depGroupName}' for '${instanceName}': ${healthError.message}`,\n );\n throw new Error(`Dependency group '${depGroupName}' for '${instanceName}' failed to become healthy.`);\n }\n }\n }\n }\n\n try {\n this.#logger.start(`Starting instance '${instanceName}' of service group '${serviceGroupName}'...`);\n await service.start();\n serviceInstances.push(service);\n } catch (startError: any) {\n this.#logger.error(\n `Failed to start instance '${instanceName}' of service group '${serviceGroupName}': ${startError.message}`,\n );\n throw startError;\n }\n }\n this.#runningServices.set(serviceGroupName, serviceInstances);\n this.#logger.success(\n `All ${replicaCount} instance(s) of service group '${serviceGroupName}' attempted to start.`,\n );\n }\n this.#logger.success(`All provided service groups attempted to start.`);\n }\n\n async streamAllLogs(): Promise<void> {\n for (const serviceInstances of this.#runningServices.values()) {\n for (const service of serviceInstances) {\n service.streamLogs().catch((err) => {\n this.#logger.error(`Error starting log stream for ${service.serviceName}: ${err.message}`);\n });\n }\n }\n }\n\n stopAllLogStreams(): void {\n for (const serviceInstances of this.#runningServices.values()) {\n for (const service of serviceInstances) {\n service.stopLogStream();\n }\n }\n }\n\n async stopAllServices(): Promise<void> {\n this.#logger.start(`Gracefully shutting down all services...`);\n this.stopAllLogStreams();\n\n // Stop services in reverse order of their startup (group-wise)\n const serviceGroupNamesToStop = Array.from(this.#runningServices.keys()).reverse();\n\n for (const serviceGroupName of serviceGroupNamesToStop) {\n const serviceInstances = this.#runningServices.get(serviceGroupName);\n if (serviceInstances) {\n this.#logger.start(`Stopping ${serviceInstances.length} instance(s) of service group '${serviceGroupName}'...`);\n // Stop instances of a group in parallel for faster shutdown\n await Promise.all(\n serviceInstances.map(async (service) => {\n await service.stop();\n await service.remove();\n }),\n );\n this.#logger.success(`All instances of service group '${serviceGroupName}' stopped and removed.`);\n }\n }\n this.#runningServices.clear();\n\n if (this.#networkId) {\n try {\n const network = this.#docker.getNetwork(this.#networkId);\n const netInfo = await network.inspect().catch(() => null);\n if (netInfo && netInfo.Containers && Object.keys(netInfo.Containers).length > 0) {\n this.#logger.warn(\n `Network '${this.#networkName}' (ID: ${this.#networkId}) still has containers: ${Object.keys(\n netInfo.Containers,\n ).join(\", \")}. Manual cleanup may be required.`,\n );\n } else if (netInfo) {\n this.#logger.info(`Removing network '${this.#networkName}' (ID: ${this.#networkId})...`);\n await network.remove();\n this.#logger.success(`Network '${this.#networkName}' removed.`);\n } else {\n this.#logger.info(\n `Network '${this.#networkName}' (ID: ${this.#networkId}) not found, likely already removed.`,\n );\n }\n } catch (error: any) {\n if (error.statusCode === 404) {\n this.#logger.info(`Network '${this.#networkName}' (ID: ${this.#networkId}) was already removed.`);\n } else {\n this.#logger.warn(`Error removing network '${this.#networkName}':`, error.message || error);\n }\n }\n this.#networkId = undefined;\n }\n this.#logger.success(`Service cleanup complete.`);\n }\n}\n","import { mkdir, writeFile as _writeFile, access } from \"node:fs/promises\";\nimport { dirname } from \"pathe\";\n\nexport async function ensureDir(dirPath: string): Promise<void> {\n if (!(await access(dirPath).catch(() => false))) {\n await mkdir(dirPath, { recursive: true });\n }\n}\n\nexport async function writeFile(filePath: string, content: string): Promise<void> {\n await ensureDir(dirname(filePath));\n await _writeFile(filePath, content.trim());\n}\n","import { exec } from \"node:child_process\";\nimport { promisify } from \"node:util\";\nimport type { ExecOptions } from \"node:child_process\";\n\nexport class TimeoutError extends Error {\n constructor(message = \"Operation timed out\") {\n super(message);\n this.name = \"TimeoutError\";\n }\n}\n\nexport class AbortError extends Error {\n constructor(message = \"Operation aborted\") {\n super(message);\n this.name = \"AbortError\";\n }\n}\n\nexport function delay(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n signal?.removeEventListener(\"abort\", onAbort);\n resolve();\n }, ms);\n\n const onAbort = () => {\n clearTimeout(timer);\n reject(new AbortError());\n };\n\n if (signal) {\n if (signal.aborted) {\n clearTimeout(timer);\n reject(new AbortError());\n } else {\n signal.addEventListener(\"abort\", onAbort);\n }\n }\n });\n}\n\nexport interface PollOptions {\n /**\n * Total timeout in milliseconds\n * @default 30000\n * */\n timeout: number;\n /**\n * Polling interval in milliseconds\n * @default 100\n * */\n interval?: number;\n /**\n * Optional AbortSignal for cancellation\n */\n signal?: AbortSignal;\n /**\n * Whether to stop polling if fn throws an error\n * @default false\n */\n stopOnError?: boolean;\n}\n\n/**\n * Polls a function until it returns true or the timeout is reached\n */\nexport async function pollFn(fn: () => Promise<boolean>, options: PollOptions): Promise<void> {\n const { timeout = 3000, interval = 100, signal, stopOnError = false } = options;\n const start = performance.now();\n\n while (performance.now() - start < timeout) {\n if (signal?.aborted) {\n throw new AbortError();\n }\n try {\n const result = await fn();\n if (result) {\n return;\n }\n } catch (err) {\n if (stopOnError) {\n throw err;\n }\n }\n await delay(interval, signal);\n }\n throw new TimeoutError();\n}\n\nexport const execAsync: typeof exec.__promisify__ = promisify(exec);\n\ninterface ExecuteCommandResult {\n stdout: string | Buffer;\n stderr: string | Buffer;\n}\nexport async function executeCommand(command: string, options?: ExecOptions): Promise<ExecuteCommandResult> {\n return execAsync(command, options);\n}\n","import { execAsync } from \"./helpers\";\n\nexport const PACT_VERSION_REGEX: RegExp = /(\\d+)\\.(\\d+)(?:\\.(\\d+))?(-[A-Za-z0-9]+)?/;\n\nexport async function isAnyPactInstalled(match?: string): Promise<boolean> {\n const version = await getCurrentPactVersion();\n return match ? (version?.includes(match) ?? false) : !!version;\n}\n\nexport async function getCurrentPactVersion(): Promise<string | undefined> {\n try {\n const { stdout } = await execAsync(\"pact --version\");\n const match = stdout.match(PACT_VERSION_REGEX);\n if (match) {\n return match[0];\n }\n } catch {\n return undefined;\n }\n}\n\nexport async function installPact(\n version?: string,\n nightly?: boolean,\n): Promise<{ stdout: string | Buffer; stderr: string | Buffer }> {\n if (nightly) {\n return execAsync(\"npx pactup install --nightly\");\n }\n\n if (version) {\n return execAsync(`npx pactup install ${version}`);\n }\n\n return execAsync(\"npx pactup install --latest\");\n}\n","import { detectPort } from \"detect-port\";\nimport { getPort } from \"get-port-please\";\n\ninterface RandomPorts {\n public: number;\n service: number;\n onDemand: number;\n stratum: number;\n p2p: number;\n}\n/**\n * Gets a series of random network ports with gaps between each.\n *\n * @param host - The host for which to get the ports. Defaults to '127.0.0.1'.\n * @param startGap - The minimum gap between successive ports. Defaults to 10.\n * @param endGap - The maximum gap between successive ports. Defaults to 100.\n * @returns An object containing the random ports assigned for public, service, on-demand, stratum, and p2p services.\n * @throws {Error} If it fails to find a suitable port for any of the services.\n */\nexport async function getRandomNetworkPorts(\n host: string = \"127.0.0.1\",\n startGap: number = 10,\n endGap: number = 100,\n): Promise<RandomPorts> {\n if (startGap <= 0 || endGap <= 0 || startGap > endGap || endGap > 65535) {\n throw new Error(\"Invalid port gap values provided.\");\n }\n\n try {\n const publicPort = await getPort({\n host,\n random: true,\n name: \"public\",\n });\n\n const service = await getPort({\n port: publicPort + startGap,\n host,\n portRange: [publicPort + startGap, publicPort + endGap],\n name: \"service\",\n });\n\n const onDemand = await getPort({\n port: service + startGap,\n host,\n portRange: [service + startGap, service + endGap],\n name: \"onDemand\",\n });\n\n const stratum = await getPort({\n port: onDemand + startGap,\n host,\n portRange: [onDemand + startGap, onDemand + endGap],\n name: \"stratum\",\n });\n\n const p2p = await getPort({\n port: stratum + startGap,\n host,\n portRange: [stratum + startGap, stratum + endGap],\n name: \"p2p\",\n });\n\n return {\n public: publicPort,\n service,\n onDemand,\n stratum,\n p2p,\n };\n } catch (error) {\n throw new Error(`Failed to get network ports: ${(error as Error).message}`);\n }\n}\n\nexport async function isPortTaken(port: number | string): Promise<boolean> {\n let p = await detectPort(port);\n if (p == port) {\n return false;\n }\n\n return true;\n}\n\nexport { detectPort } from \"detect-port\";\n\nexport { getRandomPort } from \"get-port-please\";\n","import { exec, spawn } from \"child_process\";\nimport type { ChildProcessWithoutNullStreams } from \"child_process\";\n\nimport { cleanupOnExit } from \"./cleanup\";\nimport { logger } from \"./logger\";\n\nexport interface RunBinOptions {\n silent?: boolean;\n cwd?: string;\n env?: NodeJS.ProcessEnv;\n resolveOnStart?: boolean;\n resolveIf?: (data: string) => boolean;\n}\n\nexport function runBin(\n bin: string,\n args: string[],\n options: RunBinOptions = {},\n): Promise<ChildProcessWithoutNullStreams> {\n const { cwd = process.cwd(), silent = false, env = process.env, resolveOnStart = true, resolveIf } = options;\n\n return new Promise((resolve, reject) => {\n const child = spawn(bin, args, { cwd, env });\n\n let resolved = false;\n\n const handleStdout = (data: Buffer) => {\n const output = data.toString();\n if (!silent) {\n console.log(output);\n }\n if (resolveIf && !resolved && resolveIf(output)) {\n resolved = true;\n resolve(child);\n }\n };\n\n const handleStderr = (data: Buffer) => {\n const errorOutput = data.toString();\n logger.error(errorOutput);\n };\n\n const handleError = (err: Error) => {\n // Always log errors, regardless of the 'silent' flag\n logger.error(\"Child process error:\", err);\n if (!resolved) {\n reject(err);\n }\n };\n\n const handleExit = (_code: number | null, _signal: NodeJS.Signals | null) => {\n if (!resolved) {\n resolved = true;\n resolve(child);\n }\n };\n\n child.stdout.on(\"data\", handleStdout);\n child.stderr.on(\"data\", handleStderr);\n child.on(\"error\", handleError);\n child.on(\"exit\", handleExit);\n\n // Register cleanup function for this child process\n cleanupOnExit(() => {\n if (!child.killed) {\n child.kill(\"SIGTERM\");\n }\n });\n\n if (resolveOnStart && !resolved) {\n resolved = true;\n resolve(child);\n }\n });\n}\n\nexport async function killProcess(name: string): Promise<void> {\n switch (process.platform) {\n case \"win32\":\n exec(\"taskkill /F /IM \" + name + \".exe /T\");\n break;\n default: //Linux + Darwin\n exec(\"pkill -f \" + name);\n break;\n }\n}\n","/**\n * Replaces placeholders in the given template string with corresponding values from the context.\n *\n * Placeholders are in the format `{{key}}`, where `key` corresponds to a property in the context object.\n * Whitespace around the key inside the curly braces is trimmed.\n *\n * @param template - The template string containing placeholders.\n * @param context - An object containing values to replace placeholders in the template.\n * @returns The template string with placeholders replaced by corresponding context values.\n * @throws {Error} If any placeholders remain after replacement due to missing context values.\n */\nexport function fillTemplatePlaceholders(template: string, context: Record<string, any>): string {\n const missingKeys = new Set<string>();\n\n const result = template.replace(/{{\\s*(.*?)\\s*}}/g, (_, key: string) => {\n if (Object.prototype.hasOwnProperty.call(context, key)) {\n return context[key];\n } else {\n missingKeys.add(key);\n return `{{${key}}}`; // Keep the placeholder in the result\n }\n });\n\n if (missingKeys.size > 0) {\n throw