#!/usr/bin/env node /** * Client phase-two test-environment acceptance flow. * * The platform account is used only to discover deterministic inbound fixtures. * All phase-two product assertions are executed with the Client account. * * Run directly and enter credentials when prompted: * node scripts/client-phase2-test-env-acceptance.mjs */ import { mkdir, writeFile } from 'node:fs/promises' import { randomUUID } from 'node:crypto' import { resolve } from 'node:path' import { createInterface } from 'node:readline/promises' const args = parseArgs(process.argv.slice(2)) const gateway = trimSlash(args.gateway ?? process.env.CLIENT_PHASE2_TEST_GATEWAY ?? 'http://8.163.30.128:9000') const clientUsername = args['client-username'] ?? process.env.CLIENT_PHASE2_USERNAME ?? await promptText('二期客户端账号', 'merchant_admin') const clientPassword = args['client-password'] ?? process.env.CLIENT_PHASE2_PASSWORD ?? await promptSecret('二期客户端密码') const platformUsername = args['platform-username'] ?? process.env.PLATFORM_USERNAME ?? await promptText('平台夹具查询账号', 'platform_admin') const platformPassword = args['platform-password'] ?? process.env.PLATFORM_PASSWORD ?? await promptSecret('平台夹具查询密码') const forcedOrderId = args['order-id'] ?? process.env.CLIENT_PHASE2_ORDER_ID ?? null const reportDir = resolve(args['report-dir'] ?? process.env.CLIENT_PHASE2_REPORT_DIR ?? 'reports') const timeoutMs = Number(args['timeout-ms'] ?? process.env.CLIENT_PHASE2_TIMEOUT_MS ?? 30_000) if (!clientPassword || !platformPassword) { console.error('Client and platform passwords are required') process.exit(2) } if (!Number.isFinite(timeoutMs) || timeoutMs < 1_000) { console.error('--timeout-ms must be at least 1000') process.exit(2) } const now = new Date() const marker = `${compactTimestamp(now)}-${randomUUID().slice(0, 8)}` const reportPath = resolve(reportDir, `client-phase2-test-env-${marker}.json`) const report = { marker, gateway, generatedAt: now.toISOString(), completedAt: null, success: false, fatal: null, testAccount: { username: clientUsername }, expectedTerminalState: { processStatus: 'WAIT_SPLIT', measureReviewStatus: 'APPROVED', feeStatus: 'CONFIRMED', }, resources: { tenantId: null, terminalInboundId: null, terminalOrderId: null, terminalInboundCode: null, pendingOrderId: null, foreignOrderId: null, measurementImageFileIds: [], weightImageFileIds: [], }, steps: [], } let clientToken let platformToken let terminalFixture let pendingFixture let foreignFixture let platformTerminalDetail let merchantBilling try { await sessionModule() await fixtureDiscoveryModule() await unpublishedVisibilityModule() await terminalBillingModule() await unifiedOrderModule() await isolationModule() report.success = report.steps.every((step) => step.passed) } catch (error) { report.fatal = sanitizeError(error) report.success = false } finally { report.completedAt = new Date().toISOString() await mkdir(reportDir, { recursive: true }) await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8') printSummary() } process.exitCode = report.success ? 0 : 1 async function sessionModule() { const clientLogin = await apiStep('phase2-session', 'client login', { method: 'POST', path: '/api/auth/login', body: { username: clientUsername, password: clientPassword }, }, ({ json }) => { requireSuccess(json) assertPresent(json.data?.accessToken, 'client accessToken') }, () => ({ username: clientUsername })) clientToken = clientLogin.json.data.accessToken const profile = await apiStep('phase2-session', 'load current enterprise profile', { method: 'GET', path: '/api/merchant/v1/iam/tenant/profile', token: clientToken, }, ({ json }) => { requireSuccess(json) assertPresent(json.data?.id, 'enterprise id') assertEqual(json.data?.status, 'ACTIVE', 'enterprise status') }, ({ json }) => ({ tenantId: json.data.id, tenantName: json.data.tenantName, status: json.data.status })) report.resources.tenantId = String(profile.json.data.id) await apiStep('phase2-session', 'client has order view permission', { method: 'GET', path: '/api/merchant/v1/iam/authorization/permissions', token: clientToken, }, ({ json }) => { requireSuccess(json) assertTrue(arrayData(json.data).includes('merchant:order:view'), 'merchant:order:view permission is required') }, ({ json }) => ({ permissionCount: arrayData(json.data).length })) const platformLogin = await apiStep('phase2-fixture', 'platform fixture account login', { method: 'POST', path: '/api/auth/login', body: { username: platformUsername, password: platformPassword }, }, ({ json }) => { requireSuccess(json) assertPresent(json.data?.accessToken, 'platform accessToken') }, () => ({ username: platformUsername })) platformToken = platformLogin.json.data.accessToken } async function fixtureDiscoveryModule() { const ownInbounds = await apiStep('phase2-fixture', 'discover current enterprise inbound fixtures', { method: 'GET', path: `/api/platform/inbounds?tenantId=${report.resources.tenantId}&page=1&size=200`, token: platformToken, }, ({ json }) => { requireSuccess(json) assertTrue(Array.isArray(json.data?.records), 'inbound records must be an array') }, ({ json }) => ({ total: json.data?.total, statusCounts: statusCounts(json.data?.records) })) const ownRows = pageRecords(ownInbounds.json.data) terminalFixture = forcedOrderId ? ownRows.find((row) => String(row.orderId) === String(forcedOrderId)) : ownRows.find(isTerminalFixture) if (!terminalFixture) { throw new Error(forcedOrderId ? `Configured orderId=${forcedOrderId} is not visible in tenant ${report.resources.tenantId}` : `Tenant ${report.resources.tenantId} has no WAIT_SPLIT + APPROVED + CONFIRMED fixture`) } assertTerminalFixture(terminalFixture) report.resources.terminalInboundId = String(terminalFixture.id) report.resources.terminalOrderId = String(terminalFixture.orderId) report.resources.terminalInboundCode = terminalFixture.inboundCode pendingFixture = ownRows.find((row) => String(row.orderId) !== report.resources.terminalOrderId && !isTerminalFixture(row)) report.resources.pendingOrderId = pendingFixture == null ? null : String(pendingFixture.orderId) await softApiStep('phase2-fixture', 'terminal state filters find selected inbound', { method: 'GET', path: `/api/platform/inbounds?tenantId=${report.resources.tenantId}` + '&processStatus=WAIT_SPLIT&measureReviewStatus=APPROVED&feeStatus=CONFIRMED&page=1&size=200', token: platformToken, }, ({ json }) => { requireSuccess(json) const row = pageRecords(json.data) .find((item) => String(item.id) === report.resources.terminalInboundId) assertPresent(row, 'terminal inbound filter row') assertTerminalFixture(row) }, ({ json }) => ({ total: json.data?.total, inboundId: report.resources.terminalInboundId })) const detail = await apiStep('phase2-fixture', 'load platform terminal inbound reference detail', { method: 'GET', path: `/api/platform/inbounds/${report.resources.terminalInboundId}?tenantId=${report.resources.tenantId}`, token: platformToken, }, ({ json }) => { requireSuccess(json) assertEqual(String(json.data?.summary?.id), report.resources.terminalInboundId, 'inbound id') assertEqual(String(json.data?.summary?.orderId), report.resources.terminalOrderId, 'order id') assertTerminalFixture(json.data?.summary) }, ({ json }) => ({ inboundId: json.data.summary.id, orderId: json.data.summary.orderId, processStatus: json.data.summary.processStatus, measureReviewStatus: json.data.summary.measureReviewStatus, feeStatus: json.data.summary.feeStatus, })) platformTerminalDetail = detail.json.data const allTerminal = await apiStep('phase2-fixture', 'discover foreign terminal fixture for isolation', { method: 'GET', path: '/api/platform/inbounds?page=1&size=200', token: platformToken, }, ({ json }) => { requireSuccess(json) assertTrue(Array.isArray(json.data?.records), 'terminal fixture records must be an array') }, ({ json }) => ({ total: json.data?.total })) foreignFixture = pageRecords(allTerminal.json.data) .find((row) => isTerminalFixture(row) && String(row.tenantId) !== report.resources.tenantId) report.resources.foreignOrderId = foreignFixture == null ? null : String(foreignFixture.orderId) } async function unpublishedVisibilityModule() { if (!pendingFixture) { recordSkipped('phase2-client-visibility', 'unfinished inbound billing remains unavailable', 'Current enterprise has no non-terminal inbound fixture') return } await apiStep('phase2-client-visibility', 'unfinished inbound billing remains unavailable', { method: 'GET', path: `/api/merchant/v1/first-leg/orders/${pendingFixture.orderId}/inbound-billing`, token: clientToken, }, ({ json, status }) => { assertEqual(status, 200, 'HTTP status') assertEqual(String(json?.code), 'A0404', 'business code') }, ({ json }) => ({ orderId: pendingFixture.orderId, processStatus: pendingFixture.processStatus, feeStatus: pendingFixture.feeStatus, rejectionCode: json?.code, })) await apiStep('phase2-client-visibility', 'unknown order inbound billing is rejected', { method: 'GET', path: '/api/merchant/v1/first-leg/orders/1/inbound-billing', token: clientToken, }, ({ json, status }) => { assertEqual(status, 200, 'HTTP status') assertEqual(String(json?.code), 'A0404', 'business code') }, ({ json }) => ({ orderId: 1, rejectionCode: json?.code })) } async function terminalBillingModule() { const response = await softApiStep('phase2-client-billing', 'terminal inbound billing is visible to client', { method: 'GET', path: `/api/merchant/v1/first-leg/orders/${report.resources.terminalOrderId}/inbound-billing`, token: clientToken, }, ({ json }) => { requireSuccess(json) assertPresent(json.data?.inbound?.summary, 'merchant inbound summary') assertPresent(json.data?.latestFee, 'merchant latest fee') assertEqual(String(json.data.inbound.summary.id), report.resources.terminalInboundId, 'inbound id') assertEqual(String(json.data.inbound.summary.orderId), report.resources.terminalOrderId, 'order id') assertTerminalFixture(json.data.inbound.summary) assertEqual(json.data.latestFee.feeStatus, 'CONFIRMED', 'latest fee status') }, ({ json }) => ({ inboundId: json?.data?.inbound?.summary?.id, orderId: json?.data?.inbound?.summary?.orderId, processStatus: json?.data?.inbound?.summary?.processStatus, measureReviewStatus: json?.data?.inbound?.summary?.measureReviewStatus, feeStatus: json?.data?.latestFee?.feeStatus, rejectionCode: json?.code, })) if (!response) { recordSkipped('phase2-client-billing', 'terminal billing aggregate contract', 'Terminal billing endpoint did not return a visible aggregate') recordSkipped('phase2-client-images', 'published measurement and weight image access', 'Terminal billing endpoint did not return image references') return } merchantBilling = response.json.data await localStep('phase2-client-billing', 'terminal billing aggregate contract', () => { const summary = merchantBilling.inbound.summary const fee = merchantBilling.latestFee assertStringId(summary.id, 'inbound summary id') assertStringId(summary.orderId, 'inbound order id') assertStringId(fee.feeId, 'fee id') assertStringId(fee.inboundId, 'fee inbound id') assertEqual(String(fee.inboundId), String(summary.id), 'fee inbound relation') assertEqual(fee.inboundCode, summary.inboundCode, 'fee inbound code') assertEqual(fee.customerId == null ? null : String(fee.customerId), report.resources.tenantId, 'fee customer isolation') assertTrue(Number(fee.actualWeightKg) > 0, 'actual weight must be positive') assertTrue(Number(fee.actualVolumeCbm) > 0, 'actual volume must be positive') assertTrue(Number(fee.chargeableQuantity) > 0, 'chargeable quantity must be positive') assertTrue(Number(fee.unitPrice) >= 0, 'unit price must not be negative') assertMoneyEqual(Number(fee.subtotalAmount) + Number(fee.additionalAmount), Number(fee.totalAmount), 'fee amount composition') assertEqual(fee.chargeUnit, fee.chargeMode, 'fee charge unit') assertTrue(['KG', 'CBM'].includes(fee.chargeMode), 'charge mode must be KG or CBM') }, { inboundId: report.resources.terminalInboundId, feeId: merchantBilling.latestFee.feeId, feeNo: merchantBilling.latestFee.feeNo, chargeMode: merchantBilling.latestFee.chargeMode, chargeableQuantity: merchantBilling.latestFee.chargeableQuantity, totalAmount: merchantBilling.latestFee.totalAmount, currency: merchantBilling.latestFee.currency, }) await localStep('phase2-client-billing', 'client aggregate excludes platform internal collections', () => { assertEqual(arrayData(merchantBilling.inbound.internalNotes).length, 0, 'merchant internal note count') assertEqual(arrayData(merchantBilling.inbound.statusHistory).length, 0, 'merchant status history count') for (const file of arrayData(merchantBilling.inbound.files)) { assertTrue(['PUBLIC', 'MERCHANT'].includes(file.visibility), `merchant file visibility must be PUBLIC or MERCHANT, actual=${file.visibility}`) } assertEqual(merchantBilling.inbound.publicRemark ?? null, platformTerminalDetail.publicRemark ?? null, 'public remark') }, { internalNoteCount: arrayData(merchantBilling.inbound.internalNotes).length, statusHistoryCount: arrayData(merchantBilling.inbound.statusHistory).length, visibleFileCount: arrayData(merchantBilling.inbound.files).length, }) await localStep('phase2-client-billing', 'measurement and billing basis are internally consistent', () => { const summary = merchantBilling.measurementSummary const basis = merchantBilling.billingBasis const fee = merchantBilling.latestFee assertPresent(summary?.measurementId, 'measurement id') assertEqual(summary.measurementStatus, 'SUBMITTED', 'measurement status') assertTrue(Number(summary.actualPackageCount) > 0, 'actual package count must be positive') assertTrue(Number(summary.actualGrossWeightKg) > 0, 'measurement gross weight must be positive') assertTrue(Number(summary.actualVolumeCbm) > 0, 'measurement volume must be positive') assertNumberEqual(basis.actualWeightKg, fee.actualWeightKg, 'billing actual weight') assertNumberEqual(basis.actualVolumeCbm, fee.actualVolumeCbm, 'billing actual volume') assertNumberEqual(basis.convertedQuantity, fee.convertedQuantity, 'billing converted quantity') assertNumberEqual(basis.chargeableQuantity, fee.chargeableQuantity, 'billing chargeable quantity') }, { measurementId: merchantBilling.measurementSummary?.measurementId, measurementStatus: merchantBilling.measurementSummary?.measurementStatus, measurementItemCount: arrayData(merchantBilling.measurementItems).length, weightResultCount: arrayData(merchantBilling.weightResults).length, chargeableBasis: merchantBilling.billingBasis?.chargeableBasis, }) const measurementImages = arrayData(merchantBilling.measurementImages) const weightImages = arrayData(merchantBilling.weightImages) report.resources.measurementImageFileIds = measurementImages.map((item) => String(item.fileId)) report.resources.weightImageFileIds = weightImages.map((item) => String(item.fileId)) const images = [...measurementImages, ...weightImages] if (images.length === 0) { recordSkipped('phase2-client-images', 'published measurement and weight image access', 'Terminal billing aggregate contains no image references') } else { for (const image of images.slice(0, 4)) { await apiStep('phase2-client-images', `download ${image.imageType} image ${image.fileId}`, { method: 'GET', path: apiPath(image.previewUrl), token: clientToken, responseType: 'bytes', }, ({ status, bytes, headers }) => { assertEqual(status, 200, 'HTTP status') assertTrue(bytes?.length > 0, 'downloaded image must not be empty') assertTrue((headers.get('content-type') ?? '').startsWith('image/'), 'download content type must be an image') }, ({ bytes, headers }) => ({ fileId: image.fileId, imageType: image.imageType, sizeBytes: bytes.length, contentType: headers.get('content-type'), })) } } await apiStep('phase2-client-images', 'unreferenced inbound image is rejected', { method: 'GET', path: `/api/merchant/v1/first-leg/orders/${report.resources.terminalOrderId}/inbound-billing/images/1`, token: clientToken, }, ({ json, status }) => { assertEqual(status, 200, 'HTTP status') assertEqual(String(json?.code), 'A0404', 'business code') }, ({ json }) => ({ fileId: 1, rejectionCode: json?.code })) } async function unifiedOrderModule() { const orderNo = terminalFixture.orderNo await apiStep('phase2-unified-orders', 'unified order list exposes phase-two terminal state', { method: 'GET', path: `/api/merchant/v1/orders?page=1&size=50&orderNo=${encodeURIComponent(orderNo)}`, token: clientToken, }, ({ json }) => { requireSuccess(json) const row = pageRecords(json.data) .find((item) => String(item.orderId) === report.resources.terminalOrderId) assertPresent(row, 'unified terminal order row') assertEqual(row.mainStatus, 'INBOUNDED', 'unified main status') assertEqual(String(row.inboundId), report.resources.terminalInboundId, 'unified inbound id') assertEqual(row.measurement?.reviewStatus, 'APPROVED', 'unified measurement review status') assertTrue(Number(row.actualGrossWeightKg) > 0, 'unified actual gross weight must be positive') assertTrue(Number(row.actualVolumeCbm) > 0, 'unified actual volume must be positive') }, ({ json }) => { const row = pageRecords(json.data) .find((item) => String(item.orderId) === report.resources.terminalOrderId) return { orderId: row?.orderId, mainStatus: row?.mainStatus, inboundId: row?.inboundId, measurementReviewStatus: row?.measurement?.reviewStatus, actualGrossWeightKg: row?.actualGrossWeightKg, actualVolumeCbm: row?.actualVolumeCbm } }) await apiStep('phase2-unified-orders', 'unified order detail includes phase-two timeline', { method: 'GET', path: `/api/merchant/v1/orders/${report.resources.terminalOrderId}`, token: clientToken, }, ({ json }) => { requireSuccess(json) assertEqual(String(json.data?.summary?.orderId), report.resources.terminalOrderId, 'unified order id') assertEqual(json.data?.summary?.mainStatus, 'INBOUNDED', 'unified main status') assertTrue(arrayData(json.data?.timeline).some((item) => item.status === 'INBOUNDED'), 'unified timeline must contain INBOUNDED') }, ({ json }) => ({ orderId: json.data?.summary?.orderId, mainStatus: json.data?.summary?.mainStatus, timelineCount: arrayData(json.data?.timeline).length, })) } async function isolationModule() { if (!foreignFixture) { recordSkipped('phase2-isolation', 'cross-enterprise terminal billing is rejected', 'No foreign WAIT_SPLIT + APPROVED + CONFIRMED fixture is visible to the platform account') return } await apiStep('phase2-isolation', 'cross-enterprise terminal billing is rejected', { method: 'GET', path: `/api/merchant/v1/first-leg/orders/${foreignFixture.orderId}/inbound-billing`, token: clientToken, }, ({ json, status }) => { assertEqual(status, 200, 'HTTP status') assertTrue(['A0403', 'A0404'].includes(String(json?.code)), `expected A0403/A0404, actual=${json?.code}`) }, ({ json }) => ({ orderId: foreignFixture.orderId, tenantId: foreignFixture.tenantId, rejectionCode: json?.code, })) } async function apiStep(module, name, request, validate, evidenceBuilder = defaultEvidence) { const startedAt = new Date().toISOString() const started = Date.now() let response try { response = await httpRequest(request) validate(response) report.steps.push(makeStep(module, name, true, startedAt, started, response, evidenceBuilder(response))) console.log(`PASS [${module}] ${name}`) return response } catch (error) { let evidence = null if (response) { try { evidence = evidenceBuilder(response) } catch { evidence = defaultEvidence(response) } } const step = makeStep(module, name, false, startedAt, started, response, evidence) step.error = sanitizeError(error) report.steps.push(step) console.error(`FAIL [${module}] ${name}: ${error.message}`) throw error } } async function softApiStep(module, name, request, validate, evidenceBuilder = defaultEvidence) { try { return await apiStep(module, name, request, validate, evidenceBuilder) } catch { return null } } async function localStep(module, name, validate, evidence = null) { const startedAt = new Date().toISOString() const started = Date.now() try { validate() report.steps.push(makeStep(module, name, true, startedAt, started, null, evidence)) console.log(`PASS [${module}] ${name}`) } catch (error) { const step = makeStep(module, name, false, startedAt, started, null, evidence) step.error = sanitizeError(error) report.steps.push(step) console.error(`FAIL [${module}] ${name}: ${error.message}`) throw error } } async function httpRequest({ method = 'GET', path, token, headers = {}, body, responseType = 'json' }) { const requestHeaders = { Accept: 'application/json', 'X-Request-Id': `client-phase2-${randomUUID()}`, ...headers } if (token) requestHeaders.Authorization = `Bearer ${token}` let requestBody = body if (body != null && !(body instanceof FormData)) { requestHeaders['Content-Type'] = 'application/json' requestBody = JSON.stringify(body) } const response = await fetch(`${gateway}${path}`, { method, headers: requestHeaders, body: requestBody, signal: AbortSignal.timeout(timeoutMs), }) const result = { status: response.status, headers: response.headers, json: null, bytes: null, text: null } if (responseType === 'bytes') { result.bytes = new Uint8Array(await response.arrayBuffer()) return result } const text = await response.text() result.text = text if (text) { try { result.json = JSON.parse(text) } catch { throw new Error(`Expected JSON but received ${text.slice(0, 200)}`) } } return result } function makeStep(module, name, passed, startedAt, started, response, evidence) { return { module, name, passed, skipped: false, startedAt, durationMs: Date.now() - started, http: response?.status ?? null, code: response?.json?.code == null ? null : String(response.json.code), message: response?.json?.message ?? null, requestId: headerOrBody(response, 'x-request-id', 'requestId'), traceId: headerOrBody(response, 'x-trace-id', 'traceId'), evidence, } } function recordSkipped(module, name, reason) { report.steps.push({ module, name, passed: true, skipped: true, startedAt: new Date().toISOString(), durationMs: 0, http: null, code: null, message: reason, requestId: null, traceId: null, evidence: null, }) console.log(`SKIP [${module}] ${name}: ${reason}`) } function requireSuccess(json) { assertPresent(json, 'response body') assertEqual(String(json.code), '200', 'business code') } function isTerminalFixture(row) { return row?.processStatus === 'WAIT_SPLIT' && row?.measureReviewStatus === 'APPROVED' && row?.feeStatus === 'CONFIRMED' } function assertTerminalFixture(row) { assertPresent(row, 'terminal inbound fixture') assertEqual(row.processStatus, 'WAIT_SPLIT', 'process status') assertEqual(row.measureReviewStatus, 'APPROVED', 'measurement review status') assertEqual(row.feeStatus, 'CONFIRMED', 'fee status') } function statusCounts(values) { const counts = {} for (const row of arrayData(values)) { const key = `${row.processStatus ?? 'UNKNOWN'}|${row.measureReviewStatus ?? 'NONE'}|${row.feeStatus ?? 'NONE'}` counts[key] = (counts[key] ?? 0) + 1 } return counts } function assertStringId(value, label) { assertPresent(value, label) assertEqual(typeof value, 'string', `${label} type`) assertTrue(/^\d+$/.test(value), `${label} must contain only digits`) } function assertNumberEqual(actual, expected, label) { assertEqual(Number(actual), Number(expected), label) } function assertMoneyEqual(actual, expected, label) { assertEqual(Math.round(Number(actual) * 100), Math.round(Number(expected) * 100), label) } function defaultEvidence({ json }) { if (!json || json.data == null) return null if (typeof json.data !== 'object') return { data: json.data } return null } function pageRecords(data) { if (Array.isArray(data)) return data return arrayData(data?.records ?? data?.items ?? data?.content) } function arrayData(value) { return Array.isArray(value) ? value : [] } function headerOrBody(response, headerName, bodyName) { return response?.headers?.get(headerName) ?? response?.json?.[bodyName] ?? response?.json?.data?.[bodyName] ?? null } function assertEqual(actual, expected, label) { if (actual !== expected) throw new Error(`${label}: expected=${format(expected)} actual=${format(actual)}`) } function assertPresent(value, label) { if (value == null || value === '') throw new Error(`${label} is required`) } function assertTrue(value, message) { if (!value) throw new Error(message) } function apiPath(value) { const url = new URL(String(value), `${gateway}/`) return `${url.pathname}${url.search}` } function printSummary() { const failed = report.steps.filter((step) => !step.passed) const skipped = report.steps.filter((step) => step.skipped) console.log(JSON.stringify({ success: report.success, marker, passed: report.steps.filter((step) => step.passed && !step.skipped).length, failed: failed.length, skipped: skipped.length, reportPath, }, null, 2)) } function parseArgs(values) { const parsed = {} for (let index = 0; index < values.length; index += 1) { const value = values[index] if (!value.startsWith('--')) continue const [rawKey, inlineValue] = value.slice(2).split('=', 2) parsed[rawKey] = inlineValue ?? values[index + 1] if (inlineValue == null) index += 1 } return parsed } function compactTimestamp(value) { return value.toISOString().replace(/[-:TZ.]/g, '').slice(0, 14) } function trimSlash(value) { return String(value).replace(/\/+$/, '') } function format(value) { return typeof value === 'string' ? JSON.stringify(value) : String(value) } function sanitizeError(error) { return { name: error?.name ?? 'Error', message: error?.message ?? String(error) } } async function promptText(label, defaultValue = '') { if (!process.stdin.isTTY || !process.stdout.isTTY) return defaultValue const reader = createInterface({ input: process.stdin, output: process.stdout }) try { const suffix = defaultValue ? ` [${defaultValue}]` : '' const value = await reader.question(`${label}${suffix}: `) return value.trim() || defaultValue } finally { reader.close() } } async function promptSecret(label) { if (!process.stdin.isTTY || !process.stdout.isTTY) return undefined const input = process.stdin const output = process.stdout output.write(`${label}: `) input.setRawMode(true) input.resume() input.setEncoding('utf8') let value = '' try { for await (const chunk of input) { for (const character of chunk) { if (character === '\r' || character === '\n') { output.write('\n') return value } if (character === '\u0003') throw new Error('Input cancelled') if (character === '\u007f' || character === '\b') { if (value.length > 0) value = value.slice(0, -1) continue } value += character } } return value } finally { input.setRawMode(false) input.pause() } }