@cellajs/cli 0.6.2 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +279 -9
- package/dist/a11y.d.ts +80 -0
- package/dist/a11y.js +309 -0
- package/dist/a11y.js.map +1 -0
- package/dist/adapters-CxHOtWog.d.ts +16 -0
- package/dist/bench.d.ts +179 -0
- package/dist/bench.js +1480 -0
- package/dist/bench.js.map +1 -0
- package/dist/config.js.map +1 -1
- package/dist/drive.d.ts +82 -0
- package/dist/drive.js +141 -0
- package/dist/drive.js.map +1 -0
- package/dist/index.js +6381 -782
- package/dist/index.js.map +1 -1
- package/dist/pentest.d.ts +39 -0
- package/dist/pentest.js +32 -0
- package/dist/pentest.js.map +1 -0
- package/dist/screenshots.d.ts +63 -0
- package/dist/screenshots.js +104 -0
- package/dist/screenshots.js.map +1 -0
- package/dist/style.d.ts +92 -0
- package/dist/style.js +268 -0
- package/dist/style.js.map +1 -0
- package/package.json +29 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tools/bench/run.ts","../src/utils/colors.ts","../src/utils/env.ts","../src/tools/bench/baseline.ts","../src/tools/bench/report.ts","../src/utils/layout.ts","../src/tools/bench/types.ts","../src/tools/bench/checks.ts","../src/tools/bench/http.ts","../src/tools/bench/metrics.ts","../src/tools/bench/scheduler.ts","../src/tools/bench/sse.ts","../src/tools/bench/suite.ts","../src/utils/display.ts","../package.json","../src/utils/git.ts","../src/utils/manifest.ts","../src/utils/errors.ts","../src/tools/bench/lock.ts","../src/tools/bench/request.ts"],"sourcesContent":["import { appendFileSync, mkdirSync, readdirSync, rmSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { monitorEventLoopDelay, performance } from 'node:perf_hooks';\nimport process from 'node:process';\nimport { setTimeout as delay } from 'node:timers/promises';\nimport pc from '../../utils/colors';\nimport { type BaselineEntry, loadBaseline, RUNNER_EPOCH, saveBaseline } from './baseline';\nimport { evaluateChecks } from './checks';\nimport { COUNTED, createHttpClient, errorCode } from './http';\nimport { type Distribution, type EndpointSummary, Metrics } from './metrics';\nimport { compactLine, LiveDisplay, liveLines, type Output, type SummaryView, summaryLines } from './report';\nimport { play, roleSequencer, type ScheduleEvent, schedule } from './scheduler';\nimport { readSse } from './sse';\nimport { type CheckResult, type Phase, type Scenario, splitMetricKey, type Vu, type VuFn } from './types';\n\nconst DEFAULT_INTERVAL_MS = 5000;\nconst LAG_WARN_MS = 50;\n/** The loop-delay sampler's period: it records whole periods, so an idle loop reads this much. */\nconst LAG_RESOLUTION_MS = 10;\nconst DEFAULT_DRAIN_TIMEOUT_S = 300;\n/** Seconds an aborted VU gets to unwind before the run reports without it. */\nconst ABORT_GRACE_S = 5;\n/** Logs kept per scenario; older ones are deleted. */\nconst LOGS_KEPT = 30;\n\nexport interface RunOptions {\n /** Scenario name: labels the output, the log file and the baseline history. */\n name: string;\n /** Base URL that relative request paths join. */\n target: string;\n /** Holds `baselines/` and `logs/`. */\n dataDir: string;\n short?: boolean;\n /** One result line at the end, no full summary. */\n quiet?: boolean;\n /**\n * The app's own checks of what the run did, after the load stopped. Receives the counters and the runner's checks,\n * so it can skip work on a run whose responses were already rejected.\n */\n afterRun?: (outcome: {\n counters: Record<string, number>;\n checks: CheckResult[];\n short: boolean;\n }) => Promise<CheckResult[]>;\n /** Where the live display and the summary go; stdout when unset. */\n output?: Output;\n}\n\nexport interface RunSummary {\n type: 'summary';\n runner: typeof RUNNER_EPOCH;\n scenario: string;\n startedAt: string;\n durationS: number;\n short: boolean;\n vus: { created: number; completed: number; failed: number };\n requests: number;\n requestRate: number;\n http: Distribution | null;\n endpoints: Record<string, EndpointSummary>;\n histograms: Record<string, Distribution>;\n counters: Record<string, number>;\n errors: Record<string, { count: number; message: string }>;\n checks: CheckResult[];\n generatorLagP99MaxMs: number;\n attachments: Record<string, unknown>;\n}\n\nexport interface RunResult {\n ok: boolean;\n summary: RunSummary;\n /** This run's numbers in baseline shape, also for a run that failed or was short. */\n current: BaselineEntry;\n /** The entry this run was compared against; null when none of this runner epoch exists. */\n baseline: BaselineEntry | null;\n logFile: string;\n}\n\n/** Identity: types a scenario module and checks its counter and histogram names, nothing at runtime. */\nexport function defineScenario<const C extends string = never, const H extends string = never>(\n scenario: Scenario<C, H>,\n): Scenario<C, H> {\n return scenario;\n}\n\ntype AnyScenario = Scenario<string, string>;\n\ninterface Plan {\n events: ScheduleEvent[] | Generator<ScheduleEvent>;\n phases: Phase[];\n plannedS: number | null;\n pick: () => { role: string | null; fn: VuFn<string, string> };\n}\n\n/** The arrivals of a run: the scenario's phases; one VU for `run`; in short mode one VU per role. */\nfunction plan(scenario: AnyScenario, short: boolean): Plan {\n if (scenario.run) {\n const fn = scenario.run;\n return {\n events: [\n { kind: 'phase', at: 0, phase: 0 },\n { kind: 'arrival', at: 0, phase: 0 },\n ],\n phases: [{ name: 'run', duration: 0 }],\n plannedS: null,\n pick: () => ({ role: null, fn }),\n };\n }\n const roles = scenario.roles ?? [{ name: '', weight: 1, vu: scenario.vu }];\n if (roles.some((role) => !(role.weight > 0))) throw new Error('every role needs a weight above 0');\n const next = roleSequencer(roles);\n const pick = () => {\n const role = next();\n return { role: scenario.roles ? role.name : null, fn: role.vu };\n };\n if (short) {\n let i = 0;\n const events: ScheduleEvent[] = [\n { kind: 'phase', at: 0, phase: 0 },\n ...roles.map((): ScheduleEvent => ({ kind: 'arrival', at: 0, phase: 0 })),\n ];\n const pickEach = () => {\n const role = roles[i++ % roles.length];\n return { role: scenario.roles ? role.name : null, fn: role.vu };\n };\n return { events, phases: [{ name: 'smoke', duration: 1 }], plannedS: 1, pick: pickEach };\n }\n const plannedS = scenario.phases.reduce((sum, phase) => sum + phase.duration, 0);\n return { events: schedule(scenario.phases), phases: scenario.phases, plannedS, pick };\n}\n\nfunction logFileFor(dataDir: string, name: string, startedAt: Date): string {\n const dir = join(dataDir, 'logs');\n mkdirSync(dir, { recursive: true });\n const old = readdirSync(dir)\n .filter((file) => file.startsWith(`${name}-`) && file.endsWith('.jsonl'))\n .sort();\n for (const file of old.slice(0, Math.max(0, old.length - LOGS_KEPT + 1))) rmSync(join(dir, file), { force: true });\n return join(dir, `${name}-${startedAt.toISOString().slice(0, 19).replaceAll(':', '-')}.jsonl`);\n}\n\nfunction phaseText(phase: Phase): string {\n if (phase.arrivalCount !== undefined) return `${phase.arrivalCount} VUs over ${phase.duration}s`;\n const rate = phase.arrivalRate ?? 0;\n if (phase.rampTo !== undefined && phase.rampTo !== rate) return `${rate} → ${phase.rampTo}/s over ${phase.duration}s`;\n return `${rate}/s for ${phase.duration}s`;\n}\n\n/**\n * Runs a scenario: arrivals in an open loop, a live line every interval, then the checks (the scenario's, then the\n * app's `afterRun`), the summary, a baseline comparison, and for a passing full run a new baseline entry. Every\n * interval and the summary go to a JSONL log.\n */\nexport async function runScenario(scenario: AnyScenario, options: RunOptions): Promise<RunResult> {\n const { name, target, dataDir, short = false, quiet = false } = options;\n const metrics = new Metrics();\n const output = options.output ?? process.stdout;\n const display = new LiveDisplay(Boolean(output.isTTY), output);\n const startedAt = new Date();\n const logFile = logFileFor(dataDir, name, startedAt);\n const log = (entry: object) => appendFileSync(logFile, `${JSON.stringify(entry)}\\n`);\n const { events, phases, plannedS, pick } = plan(scenario, short);\n const declared = new Set<string>(scenario.counters ?? []);\n const primaryKey = scenario.primary ?? 'http.response_time.p95';\n const { name: primaryName, stat: primaryStat } = splitMetricKey(primaryKey);\n const attachments: Record<string, unknown> = {};\n /** The first message of each error that failed a VU. */\n const errors: Record<string, { message: string }> = {};\n const showPhases = !(quiet && output.isTTY);\n\n const stop = new AbortController();\n const lag = monitorEventLoopDelay({ resolution: LAG_RESOLUTION_MS });\n lag.enable();\n let lagP99Max = 0;\n let lagWarning: string | null = null;\n\n let phaseName = phases[0]?.name ?? '';\n let active = 0;\n let index = 0;\n let onIdle: (() => void) | null = null;\n const started = performance.now();\n let lastTick = started;\n\n const launch = () => {\n const { role, fn } = pick();\n const client = createHttpClient(target, metrics, stop.signal);\n const sockets: WebSocket[] = [];\n const resolveUrl = (url: string) => (/^[a-z]+:\\/\\//.test(url) ? url : `${target}${url}`);\n const vu: Vu<string, string> = {\n index: index++,\n role,\n short,\n signal: stop.signal,\n http: client,\n sse: (url, onEvent, sseOptions = {}) => readSse(resolveUrl(url), onEvent, sseOptions, metrics, stop.signal),\n ws: (url, protocols) =>\n new Promise((resolve, reject) => {\n const socket = new WebSocket(resolveUrl(url).replace(/^http/, 'ws'), protocols);\n sockets.push(socket);\n socket.addEventListener('open', () => resolve(socket), { once: true });\n socket.addEventListener(\n 'error',\n () => reject(Object.assign(new Error(`WebSocket ${url} did not open`), { code: 'EWEBSOCKET' })),\n {\n once: true,\n },\n );\n }),\n think: (thinkMs) => delay(thinkMs, undefined, { signal: stop.signal }),\n counter: (counter, n) => metrics.counter(counter, n),\n histogram: (histogram, value) => metrics.histogram(histogram, value),\n log: (text) => display.log(text),\n attach: (key, data) => {\n attachments[key] = data;\n },\n };\n metrics.counter('vusers.created');\n active++;\n Promise.resolve()\n .then(() => fn(vu))\n .then(\n () => metrics.counter('vusers.completed'),\n (error: unknown) => {\n metrics.counter('vusers.failed');\n const code = stop.signal.aborted ? 'aborted' : errorCode(error);\n if (!(error as { [COUNTED]?: boolean })?.[COUNTED] || stop.signal.aborted) metrics.counter(`errors.${code}`);\n errors[code] ??= { message: error instanceof Error ? error.message : String(error) };\n },\n )\n .finally(async () => {\n await client.close();\n for (const socket of sockets) socket.close();\n active--;\n if (active === 0) onIdle?.();\n });\n };\n\n const tick = () => {\n const now = performance.now();\n const intervalS = (now - lastTick) / 1000;\n lastTick = now;\n const { counters, histograms } = metrics.tick();\n const lagP99 = Math.max(0, lag.percentile(99) / 1e6 - LAG_RESOLUTION_MS);\n lag.reset();\n lagP99Max = Math.max(lagP99Max, lagP99);\n const elapsedS = (now - started) / 1000;\n if (lagP99 > LAG_WARN_MS && !lagWarning) {\n lagWarning = `generator event-loop lag p99 ${Math.round(lagP99)} ms (over ${LAG_WARN_MS} ms): the load generator is saturated, latencies include its own queueing`;\n display.log(` ⚠ ${lagWarning}`);\n }\n const custom = Object.entries(counters).filter(([counter, n]) => declared.has(counter) && n > 0);\n const primary = primaryName.startsWith('http.') ? undefined : histograms[primaryName];\n const vus = {\n started: counters['vusers.created'] ?? 0,\n active,\n completed: metrics.get('vusers.completed'),\n failed: metrics.get('vusers.failed'),\n };\n log({\n type: 'interval',\n t: Math.round(elapsedS * 10) / 10,\n phase: phaseName,\n vus,\n counters,\n histograms,\n lagP99Ms: lagP99,\n });\n display.render(\n liveLines({\n scenario: name,\n phase: phaseName,\n elapsedS,\n plannedS,\n vus,\n intervalS: Math.max(intervalS, 0.001),\n counters,\n http: histograms['http.response_time'],\n primary: primary && { name: primaryName, distribution: primary },\n custom,\n lagP99,\n }),\n );\n };\n\n if (!quiet) display.log(`${pc.dim('▸')} running ${name}${short ? ' (short)' : ''}: ${scenario.description}`);\n const ticker = setInterval(tick, DEFAULT_INTERVAL_MS);\n\n await play(\n events,\n (event) => {\n if (event.kind === 'arrival') return launch();\n phaseName = phases[event.phase].name;\n if (showPhases && !scenario.run && !short)\n display.log(` ${pc.dim('phase')} ${phaseName} · ${phaseText(phases[event.phase])}`);\n },\n stop.signal,\n );\n\n if (active > 0) {\n if (!scenario.run) phaseName = 'drain';\n const drainS = scenario.drainTimeout ?? (scenario.run ? Number.POSITIVE_INFINITY : DEFAULT_DRAIN_TIMEOUT_S);\n const idle = new Promise<void>((resolve) => {\n onIdle = resolve;\n });\n // The timers are cancelled once the VUs are done: a pending one would keep the process alive.\n const timers = new AbortController();\n const wait = (seconds: number) =>\n delay(seconds * 1000, 'timeout' as const, { signal: timers.signal }).catch(() => null);\n const timedOut = Number.isFinite(drainS) ? wait(drainS) : new Promise<never>(() => {});\n if ((await Promise.race([idle, timedOut])) === 'timeout') {\n display.log(` ⚠ ${active} VUs still running after ${drainS} s: aborted`);\n stop.abort();\n await Promise.race([idle, wait(ABORT_GRACE_S)]);\n }\n timers.abort();\n }\n clearInterval(ticker);\n tick();\n lag.disable();\n const durationS = (performance.now() - started) / 1000;\n\n const counters = Object.fromEntries(metrics.counters);\n const distributions = Object.fromEntries(\n metrics.histogramNames().flatMap((histogram) => {\n const d = metrics.distribution(histogram);\n return d ? [[histogram, d]] : [];\n }),\n );\n const checks = evaluateChecks(scenario.checks ?? {}, { counters, distributions, short });\n if (options.afterRun) {\n display.render([`${name} ${pc.dim('· checking what the app recorded')}`]);\n checks.push(...(await options.afterRun({ counters, checks: [...checks], short })));\n }\n display.clear();\n const ok = checks.every((check) => check.ok);\n\n const requests = metrics.get('http.requests');\n const http = distributions['http.response_time'] ?? null;\n const endpoints = metrics.endpointSummaries();\n const current: BaselineEntry = {\n runner: RUNNER_EPOCH,\n timestamp: startedAt.toISOString(),\n scenario: name,\n requests,\n requestRate: Math.round((requests / durationS) * 10) / 10,\n mean: http?.mean ?? 0,\n p50: http?.p50 ?? 0,\n p95: http?.p95 ?? 0,\n p99: http?.p99 ?? 0,\n errors: Object.entries(counters).reduce((sum, [counter, n]) => (counter.startsWith('errors.') ? sum + n : sum), 0),\n vusersCreated: metrics.get('vusers.created'),\n vusersFailed: metrics.get('vusers.failed'),\n primary: { metric: primaryKey, value: primaryStat ? (distributions[primaryName]?.[primaryStat] ?? 0) : 0 },\n endpoints: Object.fromEntries(\n Object.entries(endpoints).map(([label, e]) => [\n label,\n {\n requests: e.requests,\n p50: e.ttfb.p50,\n p95: e.ttfb.p95,\n p99: e.ttfb.p99,\n nonOk: e.nonOk,\n timeouts: e.timeouts,\n },\n ]),\n ),\n };\n\n const { baseline, older } = short ? { baseline: null, older: 0 } : loadBaseline(dataDir, name);\n const saved = !short && ok ? saveBaseline(dataDir, current) : null;\n\n const builtin = /^(vusers|http|errors)\\./;\n const summary: RunSummary = {\n type: 'summary',\n runner: RUNNER_EPOCH,\n scenario: name,\n startedAt: startedAt.toISOString(),\n durationS,\n short,\n vus: { created: current.vusersCreated, completed: metrics.get('vusers.completed'), failed: current.vusersFailed },\n requests,\n requestRate: current.requestRate,\n http,\n endpoints,\n histograms: Object.fromEntries(\n Object.entries(distributions).filter(([histogram]) => !histogram.startsWith('http.')),\n ),\n counters,\n errors: Object.fromEntries(\n Object.entries(counters).flatMap(([counter, n]) =>\n counter.startsWith('errors.')\n ? [[counter.slice(7), { count: n, message: errors[counter.slice(7)]?.message ?? '' }]]\n : [],\n ),\n ),\n checks,\n generatorLagP99MaxMs: lagP99Max,\n attachments,\n };\n log(summary);\n\n const view: SummaryView = {\n ...summary,\n counters: Object.entries(counters).filter(([counter]) => !builtin.test(counter)),\n compared: !short && ok,\n baseline,\n olderBaselines: older,\n current,\n saved,\n logFile,\n lagWarning,\n };\n if (quiet) display.log(compactLine(view));\n else for (const line of ['', ...summaryLines(view), '']) display.log(line);\n\n return { ok, summary, current, baseline: ok ? baseline : null, logFile };\n}\n","import process from 'node:process';\nimport { styleText } from 'node:util';\nimport { hasEnv } from './env';\n\ntype ColorStyle = 'blue' | 'bold' | 'cyan' | 'dim' | 'gray' | 'green' | 'magenta' | 'red' | 'white' | 'yellow';\n\nfunction colorsEnabled(): boolean {\n if (hasEnv('NO_COLOR')) return false;\n if (hasEnv('FORCE_COLOR')) return true;\n return !!process.stdout.isTTY;\n}\n\nfunction applyStyle(style: ColorStyle, text: string): string {\n if (!colorsEnabled()) return text;\n return styleText(style, text, { validateStream: false });\n}\n\nconst pc = {\n blue: (text: string) => applyStyle('blue', text),\n bold: (text: string) => applyStyle('bold', text),\n cyan: (text: string) => applyStyle('cyan', text),\n dim: (text: string) => applyStyle('dim', text),\n gray: (text: string) => applyStyle('gray', text),\n green: (text: string) => applyStyle('green', text),\n magenta: (text: string) => applyStyle('magenta', text),\n red: (text: string) => applyStyle('red', text),\n white: (text: string) => applyStyle('white', text),\n yellow: (text: string) => applyStyle('yellow', text),\n};\n\nexport default pc;\n","import process from 'node:process';\n\n/**\n * Centralized environment access for CLI/runtime integration points.\n */\nexport function getEnv(name: string): string | undefined {\n // biome-ignore lint/style/noProcessEnv: This module centralizes intentional CLI environment access.\n return process.env[name];\n}\n\n/** Whether the variable is set at all, even to an empty string. */\nexport function hasEnv(name: string): boolean {\n // biome-ignore lint/style/noProcessEnv: This module centralizes intentional CLI environment access.\n return name in process.env;\n}\n\n/** A mutable copy of the full environment, for passing to child processes. */\nexport function getEnvSnapshot(): NodeJS.ProcessEnv {\n // biome-ignore lint/style/noProcessEnv: This module centralizes intentional CLI environment access.\n return { ...process.env };\n}\n\n/** Set a variable for this process and the children it spawns from here on. */\nexport function setEnv(name: string, value: string): void {\n // biome-ignore lint/style/noProcessEnv: This module centralizes intentional CLI environment access.\n process.env[name] = value;\n}\n","import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\n/** Marks the entries this runner wrote: an entry without it (Artillery's) stays in the file and is never compared. */\nexport const RUNNER_EPOCH = 'cella-bench@1';\n\nexport interface EndpointBaseline {\n requests: number;\n p50: number;\n p95: number;\n p99: number;\n nonOk: number;\n timeouts: number;\n}\n\n/** One passing full run. Latencies in milliseconds, `http.response_time` unless named otherwise. */\nexport interface BaselineEntry {\n runner: typeof RUNNER_EPOCH;\n timestamp: string;\n scenario: string;\n requests: number;\n requestRate: number;\n mean: number;\n p50: number;\n p95: number;\n p99: number;\n errors: number;\n vusersCreated: number;\n vusersFailed: number;\n /** The latency the scenario chose to compare first, such as `yjs.peer_latency_ms.p95`. */\n primary: { metric: string; value: number };\n endpoints: Record<string, EndpointBaseline>;\n}\n\nexport interface Delta {\n label: string;\n current: number;\n baseline: number;\n /** Percent change; null when the baseline is zero. */\n pct: number | null;\n lowerIsBetter: boolean;\n}\n\nconst isComparable = (entry: unknown): entry is BaselineEntry =>\n (entry as { runner?: unknown } | null)?.runner === RUNNER_EPOCH;\n\nfunction readEntries(file: string): unknown[] {\n if (!existsSync(file)) return [];\n try {\n const data: unknown = JSON.parse(readFileSync(file, 'utf8'));\n return Array.isArray(data) ? data : [data];\n } catch {\n return [];\n }\n}\n\nexport const baselineFile = (dir: string, scenario: string) => join(dir, 'baselines', `${scenario}.json`);\n\n/** The newest entry of this runner epoch, with the count of older-epoch entries kept beside it. */\nexport function loadBaseline(dir: string, scenario: string): { baseline: BaselineEntry | null; older: number } {\n const entries = readEntries(baselineFile(dir, scenario));\n const comparable = entries.filter(isComparable);\n return { baseline: comparable.at(-1) ?? null, older: entries.length - comparable.length };\n}\n\n/**\n * Appends a run to the scenario's history.\n * @returns How many entries the history holds.\n */\nexport function saveBaseline(dir: string, entry: BaselineEntry): number {\n const file = baselineFile(dir, entry.scenario);\n const entries = readEntries(file);\n entries.push(entry);\n mkdirSync(join(dir, 'baselines'), { recursive: true });\n writeFileSync(file, `${JSON.stringify(entries, null, 2)}\\n`);\n return entries.length;\n}\n\n/** The changes from `baseline` worth a glance: the primary latency first, then the HTTP percentiles and the rate. */\nexport function compareBaseline(current: BaselineEntry, baseline: BaselineEntry): Delta[] {\n const delta = (label: string, now: number, before: number, lowerIsBetter = true): Delta => ({\n label,\n current: now,\n baseline: before,\n pct: before === 0 ? null : ((now - before) / before) * 100,\n lowerIsBetter,\n });\n const deltas: Delta[] = [];\n if (current.primary.metric === baseline.primary.metric)\n deltas.push(delta(current.primary.metric, current.primary.value, baseline.primary.value));\n if (current.requests > 0) {\n if (current.primary.metric !== 'http.response_time.p50') deltas.push(delta('p50', current.p50, baseline.p50));\n if (current.primary.metric !== 'http.response_time.p95') deltas.push(delta('p95', current.p95, baseline.p95));\n deltas.push(\n delta('p99', current.p99, baseline.p99),\n delta('req/s', current.requestRate, baseline.requestRate, false),\n );\n }\n return deltas;\n}\n","import process from 'node:process';\nimport { stripVTControlCharacters } from 'node:util';\nimport pc from '../../utils/colors';\nimport { padVisible } from '../../utils/layout';\nimport { type BaselineEntry, compareBaseline, RUNNER_EPOCH } from './baseline';\nimport type { Distribution } from './metrics';\nimport type { RunSummary } from './run';\n\n/** Where a run prints: stdout, or a stand-in with the same three members. */\nexport interface Output {\n write(text: string): unknown;\n isTTY?: boolean;\n columns?: number;\n}\n\nconst visible = (text: string) => stripVTControlCharacters(text).length;\n\nexport const count = (n: number) => Math.round(n).toLocaleString('en-US');\n\n/** Milliseconds, one decimal below 10. */\nexport const ms = (n: number) => (n < 10 ? `${n.toFixed(1)}ms` : `${Math.round(n)}ms`);\n\nconst seconds = (n: number) => `${Math.round(n)}s`;\n\n/**\n * The live block at the bottom of the output. On a terminal each render replaces the previous block and `log` prints\n * above it; piped output gets every render appended, so a log file holds the whole run.\n */\nexport class LiveDisplay {\n private rows = 0;\n private lines: string[] = [];\n\n constructor(\n private readonly tty: boolean,\n private readonly out: Output = process.stdout,\n ) {}\n\n render(lines: string[]): void {\n this.lines = lines;\n if (!this.tty) {\n for (const line of lines) this.out.write(`${line}\\n`);\n return;\n }\n this.clear();\n this.draw();\n }\n\n log(text: string): void {\n if (!this.tty) {\n this.out.write(`${text}\\n`);\n return;\n }\n this.clear();\n this.out.write(`${text}\\n`);\n this.draw();\n }\n\n /** Removes the block from a terminal; piped output keeps what it printed. */\n clear(): void {\n if (this.rows > 0) this.out.write(`\\x1b[${this.rows}F\\x1b[J`);\n this.rows = 0;\n }\n\n private draw(): void {\n const columns = this.out.columns || 80;\n for (const line of this.lines) this.out.write(`${line}\\n`);\n this.rows = this.lines.reduce((sum, line) => sum + Math.max(1, Math.ceil(visible(line) / columns)), 0);\n }\n}\n\nexport interface LiveState {\n scenario: string;\n phase: string;\n elapsedS: number;\n /** Seconds the phases take; null for a single-run scenario. */\n plannedS: number | null;\n vus: { started: number; active: number; completed: number; failed: number };\n intervalS: number;\n counters: Record<string, number>;\n http?: Distribution;\n primary?: { name: string; distribution: Distribution };\n /** Interval deltas of the scenario's own counters. */\n custom: [string, number][];\n lagP99: number;\n}\n\nexport function liveLines(state: LiveState): string[] {\n const { vus, counters } = state;\n const time =\n state.plannedS === null ? seconds(state.elapsedS) : `${seconds(state.elapsedS)}/${seconds(state.plannedS)}`;\n const head = `${pc.bold(state.scenario)} ${pc.dim(`· ${state.phase} · ${time}`)}`;\n\n const parts = [\n `vus +${count(vus.started)} ▸${count(vus.active)} ✓${count(vus.completed)} ${vus.failed > 0 ? pc.red(`✗${count(vus.failed)}`) : '✗0'}`,\n ];\n const requests = counters['http.requests'] ?? 0;\n if (requests > 0 || state.http) {\n parts.push(`${count(requests / state.intervalS)} req/s`);\n if (state.http) parts.push(`p50 ${ms(state.http.p50)} p95 ${ms(state.http.p95)} p99 ${ms(state.http.p99)}`);\n const classes = new Map<string, number>();\n for (const [name, n] of Object.entries(counters)) {\n const status = /^http\\.codes\\.(\\d)\\d\\d$/.exec(name)?.[1];\n if (status) classes.set(`${status}xx`, (classes.get(`${status}xx`) ?? 0) + n);\n }\n const codes = [...classes]\n .sort()\n .map(([cls, n]) => (cls === '2xx' ? `${cls} ${count(n)}` : pc.yellow(`${cls} ${count(n)}`)));\n if (codes.length) parts.push(codes.join(' '));\n }\n const errors = Object.entries(counters).filter(([name]) => name.startsWith('errors.'));\n if (errors.length) parts.push(pc.red(errors.map(([name, n]) => `${name.slice(7)} ${count(n)}`).join(' ')));\n if (state.primary) {\n const d = state.primary.distribution;\n parts.push(`${state.primary.name} p50 ${ms(d.p50)} p95 ${ms(d.p95)}`);\n }\n for (const [name, n] of state.custom) parts.push(`${name} +${count(n)}`);\n parts.push(pc.dim(`gen lag ${ms(state.lagP99)}`));\n return [head, ` ${parts.join(pc.dim(' · '))}`];\n}\n\n/** The run's summary as the report prints it: the scenario's own counters, and the baseline it was compared with. */\nexport interface SummaryView\n extends Omit<RunSummary, 'type' | 'runner' | 'startedAt' | 'counters' | 'generatorLagP99MaxMs' | 'attachments'> {\n counters: [string, number][];\n /** False for a short or failed run: neither is compared. */\n compared: boolean;\n baseline: BaselineEntry | null;\n olderBaselines: number;\n current: BaselineEntry;\n saved: number | null;\n logFile: string;\n lagWarning: string | null;\n}\n\nconst pad = (text: string, width: number, left = false) => (left ? text.padEnd(width) : text.padStart(width));\n\nfunction deltaText(pct: number | null, lowerIsBetter: boolean): string {\n if (pct === null) return pc.dim('n/a');\n if (Math.abs(pct) < 1) return pc.dim('~0%');\n const label = `${pct > 0 ? '+' : '−'}${Math.abs(pct).toFixed(0)}%`;\n return (lowerIsBetter ? pct < 0 : pct > 0) ? pc.green(label) : pc.red(label);\n}\n\n/** The end-of-run report: per-endpoint table, custom histograms and counters, checks, baseline delta and log path. */\nexport function summaryLines(view: SummaryView): string[] {\n const lines: string[] = [];\n const vus = `${count(view.vus.created)} VUs (✓${count(view.vus.completed)} ✗${count(view.vus.failed)})`;\n const reqs = view.requests > 0 ? ` · ${count(view.requests)} requests · ${count(view.requestRate)} req/s` : '';\n lines.push(\n `${pc.bold(view.scenario)}${view.short ? pc.dim(' (short)') : ''} · ${view.durationS.toFixed(1)} s · ${vus}${reqs}`,\n );\n\n const labels = Object.keys(view.endpoints);\n if (labels.length) {\n const width = Math.max(8, ...labels.map((label) => label.length));\n lines.push(\n pc.dim(\n ` ${pad('endpoint', width, true)} ${pad('req', 8)} ${pad('p50', 8)} ${pad('p95', 8)} ${pad('p99', 8)} ${pad('max', 8)} ${pad('non-2xx', 8)} ${pad('timeouts', 9)}`,\n ),\n );\n for (const [label, e] of Object.entries(view.endpoints)) {\n const d = e.ttfb;\n const nonOk = e.nonOk > 0 ? pc.yellow(pad(count(e.nonOk), 8)) : pad('0', 8);\n const timeouts = e.timeouts > 0 ? pc.red(pad(count(e.timeouts), 9)) : pad('0', 9);\n lines.push(\n ` ${pad(label, width, true)} ${pad(count(e.requests), 8)} ${pad(ms(d.p50), 8)} ${pad(ms(d.p95), 8)} ${pad(ms(d.p99), 8)} ${pad(ms(d.max), 8)} ${nonOk} ${timeouts}`,\n );\n const rejected = Object.entries(e.codes).filter(([code]) => !code.startsWith('2'));\n if (rejected.length)\n lines.push(pc.dim(` ${' '.repeat(width)} ${rejected.map(([code, n]) => `${code} ×${count(n)}`).join(', ')}`));\n }\n }\n\n const histograms = Object.entries(view.histograms);\n if (histograms.length) {\n const width = Math.max(9, ...histograms.map(([name]) => name.length));\n lines.push(\n pc.dim(\n ` ${pad('histogram', width, true)} ${pad('count', 8)} ${pad('p50', 8)} ${pad('p95', 8)} ${pad('p99', 8)} ${pad('max', 8)}`,\n ),\n );\n for (const [name, d] of histograms) {\n lines.push(\n ` ${pad(name, width, true)} ${pad(count(d.count), 8)} ${pad(ms(d.p50), 8)} ${pad(ms(d.p95), 8)} ${pad(ms(d.p99), 8)} ${pad(ms(d.max), 8)}`,\n );\n }\n }\n\n if (view.counters.length)\n lines.push(` ${view.counters.map(([name, n]) => `${name} ${pc.bold(count(n))}`).join(pc.dim(' · '))}`);\n for (const [code, { count: n, message }] of Object.entries(view.errors)) {\n lines.push(pc.red(` errors.${code} ${count(n)}${message ? `: ${message}` : ''}`));\n }\n if (view.lagWarning) lines.push(pc.yellow(` ⚠ ${view.lagWarning}`));\n\n for (const check of view.checks) lines.push(` ${check.ok ? pc.green('✓') : pc.red('✗')} ${check.text}`);\n\n if (view.compared) {\n if (view.baseline) {\n const deltas = compareBaseline(view.current, view.baseline).map(\n (d) => `${d.label} ${deltaText(d.pct, d.lowerIsBetter)}`,\n );\n lines.push(\n ` baseline ${view.baseline.timestamp.slice(0, 16).replace('T', ' ')} (${RUNNER_EPOCH}): ${deltas.join(', ')}`,\n );\n } else {\n const older = view.olderBaselines > 0 ? `, ${view.olderBaselines} older-runner entries kept` : '';\n lines.push(pc.dim(` no comparable baseline: first ${RUNNER_EPOCH} run${older}`));\n }\n if (view.saved !== null) lines.push(pc.dim(` baseline saved (${view.saved} run${view.saved === 1 ? '' : 's'})`));\n }\n lines.push(pc.dim(` log ${view.logFile}`));\n return lines;\n}\n\n/** One line per scenario for `--all`: the outcome, the primary latency and what failed. */\nexport function compactLine(view: SummaryView): string {\n const failed = view.checks.filter((check) => !check.ok);\n const mark = failed.length ? pc.red('✗') : pc.green('✓');\n const reqs = view.requests > 0 ? ` · ${count(view.requests)} req · p95 ${ms(view.http?.p95 ?? 0)}` : '';\n const primary = view.current.primary.metric.startsWith('http.')\n ? ''\n : ` · ${view.current.primary.metric} ${ms(view.current.primary.value)}`;\n const reasons = failed.length ? pc.red(` · ${failed.map((check) => check.text).join('; ')}`) : '';\n return `${mark} ${view.scenario}${pc.dim(`${reqs}${primary}`)}${reasons}`;\n}\n\n/** One scenario of `--all`, as its row of the closing table reads it. */\nexport interface SuiteRow {\n name: string;\n ok: boolean;\n current: BaselineEntry;\n baseline: BaselineEntry | null;\n /** The app's own cells by column, such as how long the CDC worker needed after the run. */\n columns: Record<string, string>;\n}\n\n/** The closing table of `--all`: a row per scenario, the app's columns, and the primary latency's change. */\nexport function suiteLines(rows: SuiteRow[]): string[] {\n const extra = [...new Set(rows.flatMap((row) => Object.keys(row.columns)))];\n const heads = ['Req/s', 'p50', 'p95', 'p99', 'Errors', ...extra];\n const num = (n: number) => (n === 0 ? '-' : n < 10 ? n.toFixed(1) : String(Math.round(n)));\n const lines = ['', `${pc.bold('Summary')} ${pc.dim('· all scenarios')}`, ''];\n lines.push(\n ` ${pc.bold(pad('Scenario', 22, true))} ${heads.map((head) => pc.bold(pad(head, 8))).join(' ')} ${pc.bold(pad('Primary Δ', 10))}`,\n );\n lines.push(` ${'─'.repeat(22 + heads.length * 9 + 11)}`);\n for (const row of rows) {\n const { current, baseline } = row;\n const delta = baseline\n ? deltaText(\n baseline.primary.value === 0\n ? null\n : ((current.primary.value - baseline.primary.value) / baseline.primary.value) * 100,\n true,\n )\n : pc.dim(row.ok ? 'new' : '-');\n const cells = [current.requestRate, current.p50, current.p95, current.p99].map(num);\n cells.push(String(current.errors), ...extra.map((column) => row.columns[column] ?? '-'));\n lines.push(\n ` ${pad(row.name, 22, true)} ${cells.map((cell) => pad(cell, 8)).join(' ')} ${padVisible(delta, 10, 'end')}${row.ok ? '' : pc.red(' ✗')}`,\n );\n }\n lines.push('');\n return lines;\n}\n","/**\n * Terminal layout helpers: visible widths that ignore escape codes, column padding, cutting text\n * to the terminal width, and the short ages and counts the file rows print.\n */\n\nimport process from 'node:process';\nimport { stripVTControlCharacters } from 'node:util';\n\n/** Columns a line may use: the terminal's width, 80 when stdout is not a terminal. */\nexport function terminalColumns(): number {\n const columns = process.stdout.isTTY ? process.stdout.columns : undefined;\n return Math.max(60, columns || 80);\n}\n\n/** Width of `text` as a terminal shows it: colors and OSC 8 link targets take no columns. */\nexport function visibleWidth(text: string): number {\n return stripVTControlCharacters(text).length;\n}\n\n/** Pad `text` with spaces to `width` visible columns, on the right (`start`) or left (`end`). */\nexport function padVisible(text: string, width: number, align: 'start' | 'end' = 'start'): string {\n const padding = ' '.repeat(Math.max(0, width - visibleWidth(text)));\n return align === 'start' ? `${text}${padding}` : `${padding}${text}`;\n}\n\n/** Plain `text` cut to `width` columns, ending in `…` when cut. */\nexport function fitText(text: string, width: number): string {\n if (text.length <= width) return text;\n return `${text.slice(0, Math.max(0, width - 1)).trimEnd()}…`;\n}\n\n/** Steps git's relative dates use (`--date=relative`), in seconds, with the unit each prints in. */\nconst AGE_STEPS: Array<[below: number, unit: number, suffix: string]> = [\n [90 * 60, 60, 'm'],\n [36 * 3600, 3600, 'h'],\n [14 * 86400, 86400, 'd'],\n [70 * 86400, 7 * 86400, 'w'],\n [365 * 86400, 30 * 86400, 'mo'],\n];\n\n/** `4m`, `3h`, `2d`, `8w`, `3mo`, `2y` since `epochSeconds`; `now` under a minute. */\nexport function shortAge(epochSeconds: number, nowMs = Date.now()): string {\n const seconds = Math.max(0, Math.round(nowMs / 1000 - epochSeconds));\n if (seconds < 60) return 'now';\n for (const [below, unit, suffix] of AGE_STEPS) {\n if (seconds < below) return `${Math.max(1, Math.round(seconds / unit))}${suffix}`;\n }\n return `${Math.floor(seconds / (365 * 86400))}y`;\n}\n\n/** A line count that fits a narrow column: `909`, `9999`, `12.4k`, `311.8k`, `1.2M`. */\nexport function compactCount(count: number): string {\n if (count < 10_000) return String(count);\n if (count < 1_000_000) return `${(count / 1000).toFixed(1)}k`;\n return `${(count / 1_000_000).toFixed(1)}M`;\n}\n","import type { IncomingHttpHeaders } from 'node:http';\n\n/** A stretch of load: `arrivalRate` new VUs per second (ramping to `rampTo`), or `arrivalCount` VUs spread over `duration` seconds. */\nexport interface Phase {\n name: string;\n duration: number;\n arrivalRate?: number;\n rampTo?: number;\n arrivalCount?: number;\n}\n\n/** The statistics a threshold or a primary latency names after a histogram's name. */\nconst stats = ['p50', 'p95', 'p99', 'max', 'min', 'mean'] as const;\nexport type Stat = (typeof stats)[number];\nexport type BuiltinHistogram = 'http.response_time' | 'http.total_time';\n/** A histogram statistic by name, such as `http.response_time.p95`. */\nexport type MetricKey<H extends string> = `${BuiltinHistogram | H}.${Stat}`;\n\n/** Splits a metric key at its last dot into the histogram name and the statistic; `stat` is null when the tail names none. */\nexport function splitMetricKey(key: string): { name: string; stat: Stat | null } {\n const dot = key.lastIndexOf('.');\n const tail = key.slice(dot + 1);\n return { name: key.slice(0, dot), stat: stats.find((stat) => stat === tail) ?? null };\n}\n\n/** What a run must meet. Percentages are 0-100; latencies are milliseconds. */\nexport interface Checks<C extends string, H extends string> {\n thresholds?: Partial<Record<MetricKey<H>, number>>;\n /** Highest share of VUs that may fail: created minus completed, over created. */\n maxErrorRate?: number;\n /** Highest share of responses that may be other than 2xx; 1 when unset. */\n maxRejectedShare?: number;\n /** Counters that must have counted (full runs only: a short run plays each role once). */\n expect?: readonly C[];\n /** Counters that must stay at zero. */\n forbid?: readonly C[];\n}\n\nexport interface HttpRequestOptions {\n json?: unknown;\n body?: string | Uint8Array;\n headers?: Record<string, string>;\n /** Endpoint label for the per-endpoint table; method and path without the query when unset. */\n name?: string;\n /** Milliseconds; 10 s when unset. */\n timeout?: number;\n}\n\nexport interface HttpResponse {\n status: number;\n ok: boolean;\n headers: IncomingHttpHeaders;\n body: Buffer;\n text(): string;\n json(): unknown;\n}\n\n/** Requests over one keep-alive connection per VU. Resolves for every status; rejects on a transport error or timeout. */\nexport interface HttpClient {\n request(method: string, url: string, options?: HttpRequestOptions): Promise<HttpResponse>;\n get(url: string, options?: HttpRequestOptions): Promise<HttpResponse>;\n post(url: string, options?: HttpRequestOptions): Promise<HttpResponse>;\n put(url: string, options?: HttpRequestOptions): Promise<HttpResponse>;\n patch(url: string, options?: HttpRequestOptions): Promise<HttpResponse>;\n delete(url: string, options?: HttpRequestOptions): Promise<HttpResponse>;\n}\n\nexport interface SseEvent {\n event: string;\n data: string;\n id?: string;\n}\n\nexport interface SseOptions {\n signal?: AbortSignal;\n headers?: Record<string, string>;\n name?: string;\n}\n\n/** What one virtual user gets. A throw marks it failed and counts `errors.<code>`. */\nexport interface Vu<C extends string = string, H extends string = string> {\n /** Arrival number from 0, unique in the run. */\n index: number;\n role: string | null;\n short: boolean;\n /** Aborts when the run stops before this VU is done. */\n signal: AbortSignal;\n http: HttpClient;\n /** Reads an event stream until it ends or `signal` aborts; resolves to the response status. */\n sse(url: string, onEvent: (event: SseEvent) => void, options?: SseOptions): Promise<number>;\n /** An open WebSocket, closed when the VU ends. */\n ws(url: string, protocols?: string | string[]): Promise<WebSocket>;\n think(ms: number): Promise<void>;\n counter(name: C, n?: number): void;\n histogram(name: H, value: number): void;\n /** Prints a line without breaking the live display. */\n log(text: string): void;\n /** Adds data to the run's log under `name`. */\n attach(name: string, data: unknown): void;\n}\n\nexport type VuFn<C extends string, H extends string> = (vu: Vu<C, H>) => Promise<void>;\n\nexport interface Role<C extends string, H extends string> {\n name: string;\n weight: number;\n vu: VuFn<C, H>;\n}\n\ninterface ScenarioBase<C extends string, H extends string> {\n description: string;\n /** Every counter the scenario counts: `vu.counter`, `expect` and `forbid` accept these names only. */\n counters?: readonly C[];\n /** Every histogram the scenario records, in milliseconds. */\n histograms?: readonly H[];\n checks?: Checks<NoInfer<C>, NoInfer<H>>;\n /** The latency a baseline compares first; `http.response_time.p95` when unset. */\n primary?: MetricKey<NoInfer<H>>;\n /** Seconds the run waits for VUs in flight after the last arrival before it aborts them; 120 when unset. */\n drainTimeout?: number;\n}\n\n/** A load scenario (`phases` with `vu` or weighted `roles`), or a single-run one (`run`, called once). */\nexport type Scenario<C extends string = string, H extends string = string> = ScenarioBase<C, H> &\n (\n | { phases: Phase[]; vu: VuFn<NoInfer<C>, NoInfer<H>>; roles?: never; run?: never }\n | { phases: Phase[]; roles: Role<NoInfer<C>, NoInfer<H>>[]; vu?: never; run?: never }\n | { run: VuFn<NoInfer<C>, NoInfer<H>>; phases?: never; vu?: never; roles?: never }\n );\n\nexport interface CheckResult {\n id: string;\n ok: boolean;\n text: string;\n}\n","import type { Distribution } from './metrics';\nimport { count, ms } from './report';\nimport { type CheckResult, type Checks, splitMetricKey } from './types';\n\n/** What the checks read of a finished run. */\nexport interface CheckInput {\n counters: Record<string, number>;\n distributions: Record<string, Distribution>;\n short: boolean;\n}\n\nconst DEFAULT_MAX_REJECTED_SHARE = 1;\n\nconst pct = (n: number) => `${n.toFixed(1)}%`;\n\n/** `http.codes.<status>` counters by status. */\nfunction statusCounts(counters: Record<string, number>): [string, number][] {\n return Object.entries(counters)\n .flatMap(([name, n]): [string, number][] => {\n const status = /^http\\.codes\\.(\\d{3})$/.exec(name)?.[1];\n return status ? [[status, n]] : [];\n })\n .sort(([a], [b]) => a.localeCompare(b));\n}\n\n/**\n * Evaluates a scenario's checks. The rejected-response check always runs, short runs included: the latency of a 401\n * says nothing about the endpoint. A short run skips thresholds and `expect`.\n */\nexport function evaluateChecks(\n checks: Checks<string, string>,\n { counters, distributions, short }: CheckInput,\n): CheckResult[] {\n const results: CheckResult[] = [];\n\n const byStatus = statusCounts(counters);\n const responses = byStatus.reduce((sum, [, n]) => sum + n, 0);\n const rejected = byStatus.filter(([status]) => !status.startsWith('2'));\n const rejectedCount = rejected.reduce((sum, [, n]) => sum + n, 0);\n const maxRejected = checks.maxRejectedShare ?? DEFAULT_MAX_REJECTED_SHARE;\n if (responses > 0) {\n const ok = rejectedCount <= (responses * maxRejected) / 100;\n const detail = rejected.length ? ` (${rejected.map(([status, n]) => `${status} ×${n}`).join(', ')})` : '';\n results.push({\n id: 'responses',\n ok,\n text: `${count(rejectedCount)} of ${count(responses)} responses not 2xx, ${pct((rejectedCount / responses) * 100)} ${ok ? '≤' : '>'} ${maxRejected}%${detail}`,\n });\n }\n\n if (checks.maxErrorRate !== undefined) {\n const created = counters['vusers.created'] ?? 0;\n const completed = counters['vusers.completed'] ?? 0;\n const rate = created === 0 ? 0 : ((created - completed) / created) * 100;\n const ok = rate <= checks.maxErrorRate;\n results.push({\n id: 'errorRate',\n ok,\n text: `VU failures ${pct(rate)} ${ok ? '≤' : '>'} ${checks.maxErrorRate}% (${count(created - completed)} of ${count(created)})`,\n });\n }\n\n if (!short) {\n for (const [key, limit] of Object.entries(checks.thresholds ?? {})) {\n if (limit === undefined) continue;\n const { name, stat } = splitMetricKey(key);\n const value = stat ? distributions[name]?.[stat] : undefined;\n if (value === undefined) {\n results.push({ id: 'threshold', ok: false, text: `${key}: nothing recorded` });\n continue;\n }\n const ok = value <= limit;\n results.push({ id: 'threshold', ok, text: `${key} ${ms(value)} ${ok ? '≤' : '>'} ${ms(limit)}` });\n }\n for (const name of checks.expect ?? []) {\n const n = counters[name] ?? 0;\n results.push({ id: 'expect', ok: n > 0, text: n > 0 ? `${name} counted ${count(n)}` : `${name} stayed at 0` });\n }\n }\n\n for (const name of checks.forbid ?? []) {\n const n = counters[name] ?? 0;\n results.push({ id: 'forbid', ok: n === 0, text: n === 0 ? `${name} stayed at 0` : `${name} counted ${count(n)}` });\n }\n\n return results;\n}\n","import http from 'node:http';\nimport https from 'node:https';\nimport { performance } from 'node:perf_hooks';\nimport type { Metrics } from './metrics';\nimport type { HttpClient, HttpRequestOptions, HttpResponse } from './types';\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\n\n/** Set on an error the HTTP client already counted, so the VU that fails on it does not count it again. */\nexport const COUNTED = Symbol('counted');\n\n/** The counter suffix of an error: its system code (`ECONNRESET`), else its name. */\nexport function errorCode(error: unknown): string {\n if (error instanceof Error) return (error as NodeJS.ErrnoException).code ?? error.name;\n return 'unknown';\n}\n\nfunction codedError(code: string, message: string): Error {\n return Object.assign(new Error(message), { code });\n}\n\n/** `METHOD /path` without the query: the label of a request that names none. */\nfunction defaultLabel(method: string, url: URL): string {\n return `${method} ${url.pathname}`;\n}\n\n/**\n * An HTTP client for one VU: one keep-alive connection (`maxSockets: 1`, as Artillery gives each VU), every body read\n * to its end. `http.response_time` is the time from the request's last byte sent to the response's first byte, the\n * time to first byte as Artillery measures it; `http.total_time` runs from the call to the last body byte.\n */\nexport function createHttpClient(\n target: string,\n metrics: Metrics,\n signal: AbortSignal,\n): HttpClient & { close(): Promise<void> } {\n const agents = new Map<string, http.Agent>();\n const agentFor = (url: URL) => {\n let agent = agents.get(url.protocol);\n if (!agent) {\n agent =\n url.protocol === 'https:'\n ? new https.Agent({ keepAlive: true, maxSockets: 1 })\n : new http.Agent({ keepAlive: true, maxSockets: 1 });\n agents.set(url.protocol, agent);\n }\n return agent;\n };\n\n /** Requests not settled yet, a VU's fire-and-forget ones included. */\n const inFlight = new Set<Promise<unknown>>();\n const send = (method: string, path: string, options: HttpRequestOptions = {}): Promise<HttpResponse> => {\n const url = new URL(/^https?:\\/\\//.test(path) ? path : `${target}${path}`);\n const label = options.name ?? defaultLabel(method, url);\n const stats = metrics.endpoint(label);\n const headers: Record<string, string> = { ...options.headers };\n let body = options.body;\n if (options.json !== undefined) {\n body = JSON.stringify(options.json);\n headers['content-type'] ??= 'application/json';\n }\n if (body !== undefined) headers['content-length'] = String(Buffer.byteLength(body));\n\n return new Promise<HttpResponse>((resolve, reject) => {\n if (signal.aborted) return reject(codedError('EABORTED', 'the run stopped'));\n const started = performance.now();\n let connectedAt = 0;\n let sentAt = 0;\n let settled = false;\n const lib = url.protocol === 'https:' ? https : http;\n const req = lib.request(url, { method, headers, agent: agentFor(url) });\n\n const fail = (error: Error) => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n signal.removeEventListener('abort', onAbort);\n if (!signal.aborted) {\n const code = errorCode(error);\n if (code === 'ETIMEDOUT') {\n stats.timeouts++;\n metrics.counter('http.timeouts');\n } else stats.errors++;\n metrics.counter(`errors.${code}`);\n Object.assign(error, { [COUNTED]: true });\n }\n reject(error);\n };\n const timeoutMs = options.timeout ?? DEFAULT_TIMEOUT_MS;\n const timer = setTimeout(\n () => req.destroy(codedError('ETIMEDOUT', `${label} took longer than ${timeoutMs} ms`)),\n timeoutMs,\n );\n const onAbort = () => req.destroy(codedError('EABORTED', 'the run stopped'));\n signal.addEventListener('abort', onAbort, { once: true });\n\n req.on('socket', (socket) => {\n if (socket.connecting) socket.once('connect', () => (connectedAt = performance.now()));\n });\n req.on('finish', () => (sentAt = performance.now()));\n req.on('error', fail);\n req.on('response', (res) => {\n const firstByteAt = performance.now();\n const chunks: Buffer[] = [];\n res.on('data', (chunk: Buffer) => chunks.push(chunk));\n res.on('error', fail);\n res.on('end', () => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n signal.removeEventListener('abort', onAbort);\n const status = res.statusCode ?? 0;\n const ttfb = firstByteAt - Math.max(sentAt || started, connectedAt);\n metrics.response(label, status, ttfb, performance.now() - started);\n const buffer = Buffer.concat(chunks);\n resolve({\n status,\n ok: status >= 200 && status < 300,\n headers: res.headers,\n body: buffer,\n text: () => buffer.toString('utf8'),\n json: () => JSON.parse(buffer.toString('utf8')),\n });\n });\n });\n\n stats.requests++;\n metrics.counter('http.requests');\n req.end(body);\n });\n };\n\n const request = (method: string, path: string, options?: HttpRequestOptions) => {\n const pending = send(method, path, options);\n const settled: Promise<void> = pending.then(\n () => void inFlight.delete(settled),\n () => void inFlight.delete(settled),\n );\n inFlight.add(settled);\n return pending;\n };\n\n return {\n request,\n get: (url, options) => request('GET', url, options),\n post: (url, options) => request('POST', url, options),\n put: (url, options) => request('PUT', url, options),\n patch: (url, options) => request('PATCH', url, options),\n delete: (url, options) => request('DELETE', url, options),\n /** Closes the connections once every request has settled: destroying a socket would reset a request still on it. */\n close: async () => {\n await Promise.all(inFlight);\n for (const agent of agents.values()) agent.destroy();\n },\n };\n}\n","import { createHistogram, type RecordableHistogram } from 'node:perf_hooks';\n\n/** Percentiles of a histogram in milliseconds; zeros when it recorded nothing. */\nexport interface Distribution {\n count: number;\n min: number;\n mean: number;\n p50: number;\n p95: number;\n p99: number;\n max: number;\n}\n\n/** Histograms hold integers: values are recorded in microseconds, so sub-millisecond latencies keep their precision. */\nconst toMicros = (ms: number) => Math.max(1, Math.round(ms * 1000));\n\nfunction distribution(histogram: RecordableHistogram): Distribution {\n if (histogram.count === 0) return { count: 0, min: 0, mean: 0, p50: 0, p95: 0, p99: 0, max: 0 };\n const ms = (micros: number) => micros / 1000;\n return {\n count: histogram.count,\n min: ms(histogram.min),\n mean: ms(histogram.mean),\n p50: ms(histogram.percentile(50)),\n p95: ms(histogram.percentile(95)),\n p99: ms(histogram.percentile(99)),\n max: ms(histogram.max),\n };\n}\n\ninterface Pair {\n total: RecordableHistogram;\n interval: RecordableHistogram;\n}\n\n/** One endpoint's requests: time to first byte and to the last byte, status codes, timeouts and transport errors. */\nexport interface EndpointStats {\n requests: number;\n ttfb: RecordableHistogram;\n total: RecordableHistogram;\n codes: Map<number, number>;\n timeouts: number;\n errors: number;\n}\n\nexport interface EndpointSummary {\n requests: number;\n ttfb: Distribution;\n total: Distribution;\n codes: Record<string, number>;\n nonOk: number;\n timeouts: number;\n errors: number;\n}\n\nexport interface IntervalSnapshot {\n counters: Record<string, number>;\n histograms: Record<string, Distribution>;\n}\n\n/** Counters and histograms by name, each kept twice: for the whole run, and for the current interval until `tick`. */\nexport class Metrics {\n readonly counters = new Map<string, number>();\n private readonly intervalCounters = new Map<string, number>();\n private readonly histograms = new Map<string, Pair>();\n readonly endpoints = new Map<string, EndpointStats>();\n\n counter(name: string, n = 1): void {\n this.counters.set(name, (this.counters.get(name) ?? 0) + n);\n this.intervalCounters.set(name, (this.intervalCounters.get(name) ?? 0) + n);\n }\n\n histogram(name: string, ms: number): void {\n let pair = this.histograms.get(name);\n if (!pair) {\n pair = { total: createHistogram(), interval: createHistogram() };\n this.histograms.set(name, pair);\n }\n const value = toMicros(ms);\n pair.total.record(value);\n pair.interval.record(value);\n }\n\n endpoint(label: string): EndpointStats {\n let stats = this.endpoints.get(label);\n if (!stats) {\n stats = {\n requests: 0,\n ttfb: createHistogram(),\n total: createHistogram(),\n codes: new Map(),\n timeouts: 0,\n errors: 0,\n };\n this.endpoints.set(label, stats);\n }\n return stats;\n }\n\n /** Records a response: `http.codes.<status>`, `http.response_time` (first byte) and, when the body was read, `http.total_time`. */\n response(label: string, status: number, ttfbMs: number, totalMs?: number): void {\n const stats = this.endpoint(label);\n stats.codes.set(status, (stats.codes.get(status) ?? 0) + 1);\n stats.ttfb.record(toMicros(ttfbMs));\n this.counter('http.responses');\n this.counter(`http.codes.${status}`);\n this.histogram('http.response_time', ttfbMs);\n if (totalMs !== undefined) {\n stats.total.record(toMicros(totalMs));\n this.histogram('http.total_time', totalMs);\n }\n }\n\n get(name: string): number {\n return this.counters.get(name) ?? 0;\n }\n\n /** Whole-run distribution of a histogram; undefined for a name nothing recorded. */\n distribution(name: string): Distribution | undefined {\n const pair = this.histograms.get(name);\n return pair && distribution(pair.total);\n }\n\n histogramNames(): string[] {\n return [...this.histograms.keys()];\n }\n\n /** The interval's counters and histograms, then a fresh interval. */\n tick(): IntervalSnapshot {\n const counters = Object.fromEntries(this.intervalCounters);\n this.intervalCounters.clear();\n const histograms: Record<string, Distribution> = {};\n for (const [name, pair] of this.histograms) {\n if (pair.interval.count > 0) histograms[name] = distribution(pair.interval);\n pair.interval.reset();\n }\n return { counters, histograms };\n }\n\n endpointSummaries(): Record<string, EndpointSummary> {\n const result: Record<string, EndpointSummary> = {};\n for (const [label, stats] of this.endpoints) {\n const codes = Object.fromEntries(\n [...stats.codes].sort(([a], [b]) => a - b).map(([code, n]) => [String(code), n]),\n );\n const nonOk = [...stats.codes].filter(([code]) => code < 200 || code > 299).reduce((sum, [, n]) => sum + n, 0);\n result[label] = {\n requests: stats.requests,\n ttfb: distribution(stats.ttfb),\n total: distribution(stats.total),\n codes,\n nonOk,\n timeouts: stats.timeouts,\n errors: stats.errors,\n };\n }\n return result;\n }\n}\n","import { performance } from 'node:perf_hooks';\nimport type { Phase } from './types';\n\nexport type ScheduleEvent =\n | { kind: 'phase'; at: number; phase: number }\n | { kind: 'arrival'; at: number; phase: number };\n\n/** Seconds into a phase of its `i`-th arrival when the rate ramps linearly from `from` to `to` over `duration`. */\nfunction rampedAt(i: number, from: number, to: number, duration: number): number {\n const slope = (to - from) / (2 * duration);\n if (slope === 0) return i / from;\n return (-from + Math.sqrt(from * from + 4 * slope * i)) / (2 * slope);\n}\n\n/** How many VUs a phase starts. */\nexport function phaseArrivals(phase: Phase): number {\n if (phase.arrivalCount !== undefined) return phase.arrivalCount;\n const rate = phase.arrivalRate ?? 0;\n return Math.round(((rate + (phase.rampTo ?? rate)) / 2) * phase.duration);\n}\n\n/**\n * Every phase start and VU arrival in order, in milliseconds from the run's start. Arrivals are uniform: a fixed rate\n * spaces them `1/rate` apart from the phase start, `arrivalCount` spreads them evenly over the duration, `rampTo`\n * follows the linear rate.\n */\nexport function* schedule(phases: Phase[]): Generator<ScheduleEvent> {\n let offset = 0;\n for (const [index, phase] of phases.entries()) {\n yield { kind: 'phase', at: offset, phase: index };\n const count = phaseArrivals(phase);\n const rate = phase.arrivalRate ?? 0;\n for (let i = 0; i < count; i++) {\n let seconds: number;\n if (phase.arrivalCount !== undefined) seconds = (i * phase.duration) / count;\n else if (phase.rampTo !== undefined && phase.rampTo !== rate)\n seconds = rampedAt(i, rate, phase.rampTo, phase.duration);\n else seconds = i / rate;\n yield { kind: 'arrival', at: offset + Math.min(seconds, phase.duration) * 1000, phase: index };\n }\n offset += phase.duration * 1000;\n }\n}\n\n/** Picks roles in proportion to their weights, interleaved and exact over every full cycle (smooth weighted round-robin). */\nexport function roleSequencer<T extends { weight: number }>(roles: T[]): () => T {\n const total = roles.reduce((sum, role) => sum + role.weight, 0);\n const current = roles.map(() => 0);\n return () => {\n let best = 0;\n for (let i = 0; i < roles.length; i++) {\n current[i] += roles[i].weight;\n if (current[i] > current[best]) best = i;\n }\n current[best] -= total;\n return roles[best];\n };\n}\n\n/**\n * Plays a schedule against the clock: each event fires at its time from `performance.now()` at the start, late ones\n * at once, so timer drift never accumulates. It never waits on the VUs it starts. Resolves after the last event, or\n * when `signal` aborts.\n */\nexport function play(\n events: Iterable<ScheduleEvent>,\n onEvent: (event: ScheduleEvent) => void,\n signal: AbortSignal,\n): Promise<void> {\n const iterator = events[Symbol.iterator]();\n const start = performance.now();\n let next = iterator.next();\n return new Promise((resolve) => {\n let timer: NodeJS.Timeout | undefined;\n const stop = () => {\n clearTimeout(timer);\n resolve();\n };\n signal.addEventListener('abort', stop, { once: true });\n const tick = () => {\n const now = performance.now() - start;\n while (!next.done && next.value.at <= now && !signal.aborted) {\n onEvent(next.value);\n next = iterator.next();\n }\n if (next.done || signal.aborted) {\n signal.removeEventListener('abort', stop);\n return resolve();\n }\n timer = setTimeout(tick, Math.max(0, next.value.at - (performance.now() - start)));\n };\n tick();\n });\n}\n","import { performance } from 'node:perf_hooks';\nimport { COUNTED, errorCode } from './http';\nimport type { Metrics } from './metrics';\nimport type { SseEvent, SseOptions } from './types';\n\n/** Time to the response headers of a stream: past it the connect counts as a timeout. */\nconst CONNECT_TIMEOUT_MS = 10_000;\n\n/**\n * An incremental `text/event-stream` parser: feed it decoded text in any chunking, it calls `onEvent` per dispatched\n * event. Lines end in LF, CRLF or CR; `data` lines join with LF; comments and events without data are skipped.\n */\nexport function createSseParser(onEvent: (event: SseEvent) => void): (chunk: string) => void {\n let buffer = '';\n let event = '';\n let data: string[] = [];\n let id: string | undefined;\n\n const line = (text: string) => {\n if (text === '') {\n if (data.length > 0)\n onEvent({ event: event || 'message', data: data.join('\\n'), ...(id !== undefined && { id }) });\n event = '';\n data = [];\n return;\n }\n if (text.startsWith(':')) return;\n const colon = text.indexOf(':');\n const field = colon === -1 ? text : text.slice(0, colon);\n const value = colon === -1 ? '' : text.slice(colon + 1).replace(/^ /, '');\n if (field === 'event') event = value;\n else if (field === 'data') data.push(value);\n else if (field === 'id') id = value;\n };\n\n return (chunk) => {\n buffer += chunk;\n let start = 0;\n for (let i = 0; i < buffer.length; i++) {\n const char = buffer[i];\n if (char !== '\\n' && char !== '\\r') continue;\n // A CR that ends the chunk may be the first half of a CRLF: the next chunk decides.\n if (char === '\\r' && i === buffer.length - 1) break;\n line(buffer.slice(start, i));\n if (char === '\\r' && buffer[i + 1] === '\\n') i++;\n start = i + 1;\n }\n buffer = buffer.slice(start);\n };\n}\n\n/**\n * Reads an event stream with `fetch` until it ends or a signal aborts. The connect counts as a request of its endpoint\n * (status, time to headers); a transport error rejects, a non-2xx status resolves at once.\n */\nexport async function readSse(\n url: string,\n onEvent: (event: SseEvent) => void,\n options: SseOptions,\n metrics: Metrics,\n vuSignal: AbortSignal,\n): Promise<number> {\n const label = options.name ?? `SSE ${new URL(url).pathname}`;\n const stats = metrics.endpoint(label);\n const connect = new AbortController();\n const signal = AbortSignal.any([vuSignal, connect.signal, ...(options.signal ? [options.signal] : [])]);\n const timer = setTimeout(\n () =>\n connect.abort(\n Object.assign(new Error(`${label} sent no headers in ${CONNECT_TIMEOUT_MS} ms`), { code: 'ETIMEDOUT' }),\n ),\n CONNECT_TIMEOUT_MS,\n );\n const started = performance.now();\n stats.requests++;\n metrics.counter('http.requests');\n\n let res: Response;\n try {\n res = await fetch(url, { headers: { accept: 'text/event-stream', ...options.headers }, signal });\n } catch (error) {\n clearTimeout(timer);\n if (options.signal?.aborted || vuSignal.aborted) return 0;\n const cause = connect.signal.aborted ? connect.signal.reason : error;\n const code = errorCode(cause);\n if (code === 'ETIMEDOUT') {\n stats.timeouts++;\n metrics.counter('http.timeouts');\n } else stats.errors++;\n metrics.counter(`errors.${code}`);\n throw Object.assign(cause instanceof Error ? cause : new Error(String(cause)), { [COUNTED]: true });\n }\n clearTimeout(timer);\n metrics.response(label, res.status, performance.now() - started);\n if (!res.ok || !res.body) {\n await res.body?.cancel();\n return res.status;\n }\n\n const push = createSseParser(onEvent);\n const decoder = new TextDecoder();\n try {\n for await (const chunk of res.body) push(decoder.decode(chunk, { stream: true }));\n } catch (error) {\n if (!signal.aborted) throw error;\n }\n return res.status;\n}\n","import { readdirSync } from 'node:fs';\nimport { basename, join } from 'node:path';\nimport process from 'node:process';\nimport { setTimeout as sleep } from 'node:timers/promises';\nimport { pathToFileURL } from 'node:url';\nimport { select } from '@inquirer/prompts';\nimport pc from '../../utils/colors';\nimport { createSpinner, MENU_DIVIDER, printStep, spinnerFail, warningMark } from '../../utils/display';\nimport { getEnv } from '../../utils/env';\nimport { errorMessage } from '../../utils/errors';\nimport { acquireLock } from './lock';\nimport { type SuiteRow, suiteLines } from './report';\nimport { BENCH_REQUEST_ENV, type BenchRequest } from './request';\nimport { runScenario } from './run';\nimport type { CheckResult, Scenario } from './types';\n\n/** After a saturating scenario, a 5 s pause left sse-fanout's p95 ten times higher. */\nconst DEFAULT_COOLDOWN_S = 15;\n\n/** What a scenario's run left for the app to check. */\nexport interface RunOutcome {\n counters: Record<string, number>;\n /** The runner's checks: a run whose responses were rejected may skip the app's. */\n checks: CheckResult[];\n short: boolean;\n}\n\nexport interface AppChecks {\n checks: CheckResult[];\n /** Cells of the `--all` table by column name. */\n columns?: Record<string, string>;\n}\n\n/** The app's side of a bench run: the stack it measures and its hooks around the scenarios. */\nexport interface Bench<State = undefined> {\n /** Base URL that relative request paths join; one run at a time holds it. */\n target: string;\n /** Waits for the stack to answer; throws what is missing. */\n preflight?: () => Promise<void>;\n /** Writes the data the scenarios read, unless `--skip-seed`. */\n seed?: () => Promise<void>;\n /** After the seed, and with `--skip-seed` too: a throw stops the run before anything is measured. */\n beforeAll?: (options: { skipSeed: boolean }) => Promise<void>;\n /** Before each scenario; what it returns reaches `afterEach`. */\n beforeEach?: (scenario: string) => Promise<State>;\n /** The app's checks of what the stack did with a scenario's load. `state` is undefined without `beforeEach`. */\n afterEach?: (outcome: RunOutcome, state: State | undefined) => Promise<AppChecks>;\n /** Seconds between the full runs of `--all`; 15 when unset. */\n cooldownSeconds?: number;\n}\n\nfunction readRequest(): BenchRequest {\n const raw = getEnv(BENCH_REQUEST_ENV);\n if (!raw) throw new Error('this is the app side of `pnpm cella bench`: run that instead');\n return JSON.parse(raw);\n}\n\nconst isScenario = (value: unknown): value is Scenario =>\n typeof value === 'object' && value !== null && 'description' in value && typeof value.description === 'string';\n\nasync function loadScenario(dir: string, name: string): Promise<Scenario> {\n const { default: scenario } = await import(pathToFileURL(join(dir, `${name}.ts`)).href);\n if (!isScenario(scenario)) throw new Error(`scenarios/${name}.ts has no \\`export default defineScenario(...)\\``);\n return scenario;\n}\n\n/** A scenario picked from a list of all of them with their descriptions; undefined for exit. */\nasync function pickScenario(dir: string): Promise<string | undefined> {\n const names = readdirSync(dir)\n .filter((file) => file.endsWith('.ts'))\n .map((file) => basename(file, '.ts'))\n .sort();\n const choices = [];\n for (const name of names) {\n const { description } = await loadScenario(dir, name);\n choices.push({ value: name, name: `${name.padEnd(22)}${pc.dim(description)}` });\n }\n const picked = await select({\n message: 'select a scenario',\n choices: [\n ...choices,\n { type: 'separator' as const, separator: MENU_DIVIDER },\n { value: '', name: pc.red(`${'exit'.padEnd(22)}${pc.dim('quit without running')}`) },\n ],\n loop: false,\n });\n return picked || undefined;\n}\n\nasync function runRequest<State>(bench: Bench<State>, request: BenchRequest): Promise<number> {\n const picked = request.scenarios.length > 0 ? undefined : await pickScenario(request.scenariosDir);\n const names = picked ? [picked] : request.scenarios;\n if (names.length === 0) return 0;\n const scenarios: [name: string, scenario: Scenario][] = [];\n for (const name of names) scenarios.push([name, await loadScenario(request.scenariosDir, name)]);\n\n if (bench.preflight) {\n createSpinner(`checking the stack at ${bench.target}`);\n await bench.preflight();\n }\n printStep('stack', bench.target);\n\n // A second run would seed and reset under the first one's measurement\n const claimed = acquireLock(request.dataDir, bench.target, request.all ? '--all' : names[0]);\n if ('holder' in claimed) {\n const { pid, scenario, startedAt } = claimed.holder;\n const message = `another bench run holds ${bench.target}: pid ${pid} runs ${scenario} since ${new Date(startedAt).toLocaleTimeString('en-GB')}`;\n throw new Error(message);\n }\n const { lock } = claimed;\n for (const other of lock.others) {\n console.info(\n `${warningMark} bench pid ${other.pid} runs ${other.scenario} on ${other.target}: it shares this machine, numbers will be skewed`,\n );\n }\n\n if (bench.seed && !request.skipSeed) {\n createSpinner('seeding the bench data');\n await bench.seed();\n printStep('seeded', pc.dim('bench data written again'));\n }\n await bench.beforeAll?.({ skipSeed: request.skipSeed });\n\n const cooldownS = request.all && !request.short ? (bench.cooldownSeconds ?? DEFAULT_COOLDOWN_S) : 0;\n const rows: SuiteRow[] = [];\n for (const [index, [name, scenario]] of scenarios.entries()) {\n lock.update(name);\n const state = await bench.beforeEach?.(name);\n let columns: Record<string, string> = {};\n const result = await runScenario(scenario, {\n name,\n target: bench.target,\n dataDir: request.dataDir,\n short: request.short,\n quiet: request.all,\n afterRun: async (outcome) => {\n if (!bench.afterEach) return [];\n const app = await bench.afterEach(outcome, state);\n columns = app.columns ?? {};\n return app.checks;\n },\n });\n rows.push({ name, ok: result.ok, current: result.current, baseline: result.baseline, columns });\n\n if (cooldownS > 0 && index < scenarios.length - 1) {\n const spinner = createSpinner(`cooling down ${cooldownS} s`);\n await sleep(cooldownS * 1000);\n spinner.stop();\n }\n }\n lock.release();\n\n const failed = rows.filter((row) => !row.ok).map((row) => row.name);\n if (request.all && !request.short) for (const line of suiteLines(rows)) console.info(line);\n else if (request.all && failed.length === 0) console.info(`\\n${pc.green('✓')} every scenario passed its short run`);\n else if (request.all) console.info(`\\n${pc.red('✗')} the short run failed for ${failed.join(', ')}`);\n return failed.length > 0 ? 1 : 0;\n}\n\n/**\n * The app's bench entry: runs what `pnpm cella bench` asked for with the app's hooks. The preflight, a lock on the\n * target, the seed and `beforeAll` come first; then each scenario between `beforeEach` and `afterEach`, the app's checks\n * joining the scenario's own. Sets the exit code: 1 when a scenario failed or a hook threw.\n */\nexport async function runBench<State = undefined>(bench: Bench<State>): Promise<void> {\n process.once('SIGINT', () => process.exit(130));\n process.once('SIGTERM', () => process.exit(143));\n try {\n process.exitCode = await runRequest(bench, readRequest());\n } catch (error) {\n spinnerFail(errorMessage(error));\n process.exitCode = 1;\n }\n}\n","/**\n * Display utilities: console output formatting, spinners, hyperlinks, and the\n * report blocks analyze and sync print.\n */\n\nimport { resolve } from 'node:path';\nimport process from 'node:process';\nimport packageJson from '../../package.json' with { type: 'json' };\nimport type { AnalysisSummary, AnalyzedFile, CommitRangeEntry, FileStatus, MergeResult } from '../config/types';\nimport pc from './colors';\nimport { getEnv } from './env';\nimport { splitPrNumber } from './git';\nimport { fitText, padVisible, shortAge, terminalColumns } from './layout';\nimport { CONFIG_FILE, isGeneratedFile, isManagedFile } from './managed-files';\n\n/** Version from package.json */\nexport const VERSION = packageJson.version;\n\n/** Options for generating links in CLI output */\nexport interface LinkOptions {\n /** Base GitHub URL for upstream (e.g., 'https://github.com/cellajs/cella') */\n upstreamGitHubUrl?: string;\n /** Upstream commit the run analyzed: file links show the file as it is there */\n upstreamSha?: string;\n /** Base GitHub URL of the fork's `origin`, for links to the fork's own commits */\n forkGitHubUrl?: string;\n /** File link mode: 'commit' links to commit, 'file' links to file in repo */\n fileLinkMode?: 'commit' | 'file';\n /** Fork repository path for resolving file paths */\n forkPath?: string;\n}\n\n/** Line divider */\nexport const DIVIDER = '─'.repeat(60);\n\n/** Divider used above the exit entry of interactive menus */\nexport const MENU_DIVIDER = '─'.repeat(40);\n\n/** Most recent upstream commits listed in the sync PR body */\nexport const COMMIT_LIST_MAX = 50;\n\n/** Most recent upstream commits listed under the `upstream` step */\nexport const COMMIT_STEP_MAX = 10;\n\n/** Width of the label column of step lines (`✓ upstream …`) */\nconst STEP_LABEL_WIDTH = 9;\n\n/** Where the detail of a step line starts: the mark, the label column and two spaces */\nexport const STEP_INDENT = 2 + STEP_LABEL_WIDTH + 2;\n\n/** Warning mark for non-fatal warnings */\nexport const warningMark = pc.yellow('⚠');\n\n/** Check mark for successful status lines */\nexport const checkMark = pc.green('✓');\n\n/** A progress line: `text` is read on every frame, `stop()` clears the line. */\ntype Spinner = { text: string; stop(): void };\n\n/** Active spinner reference */\nlet activeSpinner: Spinner | null = null;\n\n/**\n * When true, stdout is reserved for machine-readable payloads (e.g. `--json`),\n * so all human-facing output (header, warnings, steps, spinner) is routed to stderr.\n */\nlet jsonMode = false;\n\n/**\n * Enable JSON mode: keep stdout clean for the JSON payload by sending every\n * human-facing line (console.info/console.warn, header, spinner) to stderr.\n * This makes `cella --json ... | jq` pipe cleanly.\n */\nexport function setJsonMode(enabled: boolean): void {\n jsonMode = enabled;\n if (!enabled) return;\n const toStderr = (...args: unknown[]) => {\n process.stderr.write(`${args.map((a) => (typeof a === 'string' ? a : String(a))).join(' ')}\\n`);\n };\n console.info = toStderr;\n console.warn = toStderr;\n}\n\n/** Write a machine-readable payload to stdout (bypasses the stderr routing of JSON mode). */\nexport function writeStdout(text: string): void {\n process.stdout.write(text.endsWith('\\n') ? text : `${text}\\n`);\n}\n\nexport function printHeader(): void {\n const left = '⧈ cella cli';\n const version = ` · v${VERSION}`;\n const right = 'cellajs.com';\n // Padding counts the visible characters only, not the ANSI codes\n const padding = Math.max(1, DIVIDER.length - `${left}${version}${right}`.length);\n console.info();\n console.info(`${pc.cyan(left)}${pc.dim(version)}${pc.cyan(`${' '.repeat(padding)}${right}`)}`);\n console.info(DIVIDER);\n console.info();\n}\n\n/** Start a progress spinner; suppressed in test environments and JSON mode. */\nexport function createSpinner(text: string): Spinner {\n // One line holds one spinner: a new one replaces the one still running\n stopSpinner();\n const enabled = !jsonMode && !getEnv('VITEST') && getEnv('NODE_ENV') !== 'test' && !!process.stdout.isTTY;\n const frames = ['-', '\\\\', '|', '/'];\n let frame = 0;\n let timer: ReturnType<typeof setInterval> | undefined;\n const spinner: Spinner = {\n text,\n stop() {\n clearInterval(timer);\n if (enabled) process.stdout.write('\\r\\x1b[2K');\n },\n };\n const render = () => process.stdout.write(`\\r${pc.cyan(frames[frame++ % frames.length])} ${spinner.text}`);\n if (enabled) {\n render();\n timer = setInterval(render, 80);\n }\n activeSpinner = spinner;\n return spinner;\n}\n\n/**\n * Print a completed step of the merge engine on one line, `✓ label detail`; further lines of\n * `detail` line up under its first.\n */\nexport function printStep(label: string, detail?: string, mark = checkMark): void {\n stopSpinner();\n const [first = '', ...rest] = (detail ?? '').split('\\n');\n console.info(`${mark} ${pc.bold(label.padEnd(STEP_LABEL_WIDTH))} ${first}`.trimEnd());\n for (const line of rest) console.info(line ? `${' '.repeat(STEP_INDENT)}${line}` : '');\n}\n\n/** Stop the spinner and print `message` as a completed step; `detail` adds a dim line and a blank line. */\nexport function spinnerSuccess(message?: string, detail?: string): void {\n stopSpinner();\n if (!message) return;\n console.info(`${checkMark} ${message}`);\n if (detail) {\n console.info(` ${pc.dim(detail)}`);\n console.info();\n }\n}\n\nexport function spinnerFail(message: string): void {\n stopSpinner();\n console.info(`${pc.red('✗')} ${message}`);\n}\n\nexport function spinnerText(text: string): void {\n if (activeSpinner) {\n activeSpinner.text = text;\n }\n}\n\nfunction stopSpinner(): void {\n if (activeSpinner) {\n activeSpinner.stop();\n activeSpinner = null;\n }\n}\n\n/**\n * Create a clickable hyperlink for terminals that support OSC 8.\n * Falls back to just the label if no URL provided.\n */\nexport function hyperlink(label: string, url?: string): string {\n if (!url) return label;\n // Strip control characters from URL to prevent terminal escape injection (CWE-116)\n // biome-ignore lint/suspicious/noControlCharactersInRegex: stripping control chars is the security purpose here.\n const safeUrl = url.replace(/[\\x00-\\x1f\\x7f]/g, '');\n // OSC 8 hyperlink format: \\x1b]8;;URL\\x07LABEL\\x1b]8;;\\x07\n return `\\x1b]8;;${safeUrl}\\x07${label}\\x1b]8;;\\x07`;\n}\n\n/** A commit subject colored by kind: releases dim, breaking changes (`feat!:`) with a yellow type. */\nfunction styleSubject(subject: string): string {\n if (/^chore(\\([^)]*\\))?: release\\b/.test(subject)) return pc.dim(subject);\n const breaking = /^([a-z]+(\\([^)]*\\))?!:)/.exec(subject);\n return breaking ? `${pc.yellow(breaking[1])}${subject.slice(breaking[1].length)}` : subject;\n}\n\n/**\n * The `checkout` step: branch and commit, whether the tree is clean, and how the branch stands to\n * its remote when it is ahead or behind. The path shows when it is not the current directory, as\n * when `forks` syncs another repo.\n */\nexport function formatCheckoutStep(checkout: {\n forkPath: string;\n branch: string;\n headSha: string;\n changeCount: number;\n upstream: { upstream: string | null; ahead: number; behind: number };\n}): string {\n const { forkPath, branch, headSha, changeCount, upstream } = checkout;\n const parts = [\n changeCount === 0 ? 'clean' : `${plural(changeCount, 'uncommitted change')}, not included in the analysis`,\n ];\n if (upstream.upstream && (upstream.ahead > 0 || upstream.behind > 0)) {\n parts.push(`${upstream.ahead} ahead, ${upstream.behind} behind ${upstream.upstream}`);\n }\n const line = `${branch} @ ${headSha}${pc.dim(` · ${parts.join(' · ')}`)}`;\n return resolve(forkPath) === resolve(process.cwd()) ? line : `${line}\\n${pc.dim(forkPath)}`;\n}\n\n/**\n * The `upstream` step: the branch or release upgraded to and how many commits it brings since the\n * last upgrade, then those commits (see {@link formatUpstreamCommits}).\n */\nexport function formatUpstreamStep(upstream: {\n label: string;\n sha: string;\n baseSha: string;\n pinned: boolean;\n commitCount: number;\n commits: CommitRangeEntry[];\n githubUrl?: string;\n}): string {\n const { label, sha, baseSha, pinned, commitCount, commits, githubUrl } = upstream;\n const since =\n commitCount === 0\n ? 'no new commits since the last upgrade'\n : `${plural(commitCount, 'new commit')} since the last upgrade at ${baseSha}`;\n return [\n `${label} @ ${sha}${pc.dim(`${pinned ? ' · --ref' : ''} · ${since}`)}`,\n ...formatUpstreamCommits(commitCount, commits, githubUrl),\n ].join('\\n');\n}\n\n/**\n * The upstream commits under the `upstream` step: newest first, one line each with the short hash,\n * the subject cut to the terminal width, the PR number and the age; hash and PR link to GitHub.\n * `commits` comes oldest first, as `listCommitsBetween` returns it.\n */\nexport function formatUpstreamCommits(\n commitCount: number,\n commits: CommitRangeEntry[],\n upstreamGitHubUrl?: string,\n options: { columns?: number; max?: number; nowMs?: number } = {},\n): string[] {\n const { columns = terminalColumns(), max = COMMIT_STEP_MAX, nowMs } = options;\n const shown = commits.slice(-max).reverse();\n const rows = shown.map((commit) => ({ commit, ...splitPrNumber(commit.message) }));\n const prWidth = Math.max(0, ...rows.map((row) => (row.pr ? `#${row.pr}`.length : 0)));\n const ages = rows.map((row) => (row.commit.timestamp ? shortAge(row.commit.timestamp, nowMs) : ''));\n const ageWidth = Math.max(0, ...ages.map((age) => age.length));\n const subjectWidth = Math.max(\n 20,\n columns - STEP_INDENT - 7 - 2 - (prWidth ? prWidth + 2 : 0) - (ageWidth ? ageWidth + 2 : 0),\n );\n\n const lines = rows.map((row, index) => {\n const hash = hyperlink(\n row.commit.hash.slice(0, 7),\n upstreamGitHubUrl && `${upstreamGitHubUrl}/commit/${row.commit.hash}`,\n );\n const subject = fitText(row.subject, subjectWidth);\n let line = `${pc.dim(hash)} ${padVisible(styleSubject(subject), subjectWidth)}`;\n if (prWidth) {\n const pr = row.pr ? hyperlink(`#${row.pr}`, upstreamGitHubUrl && `${upstreamGitHubUrl}/pull/${row.pr}`) : '';\n line += ` ${pc.dim(padVisible(pr, prWidth))}`;\n }\n if (ageWidth) line += ` ${pc.dim(ages[index].padStart(ageWidth))}`;\n return line.trimEnd();\n });\n if (commitCount > shown.length) lines.push(pc.dim(`+ ${commitCount - shown.length} earlier`));\n return lines;\n}\n\n/** One step of `next steps`: a label for what to do, the command or link, and lines under it. */\nexport interface NextStep {\n label: string;\n line: string;\n details?: string[];\n}\n\n/**\n * The `next steps` section that closes analyze and sync, numbered in the order to take them. The\n * line and details of each step start at the column the engine's step details do.\n */\nexport function printNextSteps(steps: NextStep[]): void {\n if (steps.length === 0) return;\n printSectionHeader(pc.cyan('next steps'));\n for (const [index, step] of steps.entries()) {\n const label = step.label.padEnd(STEP_INDENT - 5);\n console.info(` ${pc.dim(String(index + 1))} ${pc.bold(label)} ${step.line}`.trimEnd());\n for (const detail of step.details ?? []) console.info(`${' '.repeat(STEP_INDENT)}${detail}`);\n }\n}\n\n/** A section header: a blank line, the title, the divider under it, and a blank line. */\nexport function printSectionHeader(title: string): void {\n console.info();\n console.info(title);\n console.info(DIVIDER);\n console.info();\n}\n\nexport function formatMergeInProgressDetail(\n conflictCount: number,\n autoMergedFiles: string[],\n linkOptions: LinkOptions,\n maxFiles = 100,\n): string {\n const lines = [`${plural(conflictCount, 'conflict')} to resolve in your editor`];\n\n if (autoMergedFiles.length === 0) {\n return lines.join('\\n');\n }\n\n lines.push('');\n lines.push(` auto-merged files (${autoMergedFiles.length}):`);\n\n // Each file opens in VS Code from the fork workspace when the fork path is known\n const { forkPath } = linkOptions;\n const shown = autoMergedFiles.slice(0, maxFiles);\n for (const filePath of shown) {\n lines.push(` - ${forkPath ? hyperlink(filePath, `vscode://file${resolve(forkPath, filePath)}`) : filePath}`);\n }\n\n if (autoMergedFiles.length > shown.length) {\n lines.push(` ... + ${autoMergedFiles.length - shown.length} more`);\n }\n\n return lines.join('\\n');\n}\n\n/** Display configuration for a file status */\ninterface StatusConfig {\n icon: string;\n label: string;\n color: (text: string) => string;\n description?: string;\n}\n\ntype SummaryStatus = FileStatus | 'managed';\n\n/** Unified status display config: icon, label, color, and description for each status. */\nconst statusConfig: Record<FileStatus, StatusConfig> = {\n behind: { icon: pc.cyan('↓'), label: 'behind', color: pc.cyan, description: 'upstream changed, will update' },\n diverged: { icon: pc.magenta('⇅'), label: 'diverged', color: pc.magenta, description: 'both changed, will merge' },\n drifted: { icon: pc.yellow('!'), label: 'drifted', color: pc.yellow, description: 'fork changed (at risk)' },\n ahead: { icon: pc.blue('↑'), label: 'ahead', color: pc.blue, description: 'fork changed (protected)' },\n local: { icon: pc.green('+'), label: 'local', color: pc.green, description: 'local file in cella folders' },\n pinned: { icon: pc.green('⨀'), label: 'pinned', color: pc.green, description: 'both changed, fork wins' },\n ignored: { icon: pc.gray('⨂'), label: 'ignored', color: pc.gray },\n identical: { icon: pc.gray('✓'), label: 'identical', color: pc.gray, description: 'no changes' },\n deleted: { icon: pc.red('✗'), label: 'deleted', color: pc.red },\n renamed: { icon: pc.blue('→'), label: 'renamed', color: pc.blue },\n};\n\nconst summaryStatusConfig: Record<SummaryStatus, StatusConfig> = {\n ...statusConfig,\n managed: {\n icon: pc.cyan('◇'),\n label: 'managed',\n color: pc.cyan,\n description: 'package.jsons & cella/cella.config.ts handled separately',\n },\n ignored: { ...statusConfig.ignored, description: 'protected by ignored config' },\n};\n\n/** The grouped count of every status, under its own section header. */\nexport function printSummary(summary: AnalysisSummary, title = 'summary'): void {\n printSectionHeader(pc.cyan(title));\n\n const maxCount = Math.max(...Object.values(summary));\n const countWidth = String(maxCount).length + 1;\n\n // Grouped layout with blank lines between groups\n const groups: SummaryStatus[][] = [\n ['managed', 'ignored'],\n ['identical'],\n ['ahead', 'local', 'drifted'],\n ['diverged', 'pinned'],\n ['behind'],\n ];\n for (const group of groups) {\n if (group !== groups[0]) console.info();\n for (const status of group) {\n const { icon, label, color, description } = summaryStatusConfig[status];\n const countStr = color(String(summary[status]).padStart(countWidth));\n console.info(` ${icon} ${countStr} ${label.padEnd(15)}${description ? pc.dim(description) : ''}`);\n }\n }\n}\n\n/** Files with `status`, managed files left out: the list each analyze file group prints. */\nexport function filesWithStatus(files: AnalyzedFile[], status: FileStatus): AnalyzedFile[] {\n return files.filter((f) => f.status === status && !isManagedFile(f.path));\n}\n\n/**\n * Protected (pinned/ignored) files that upstream also changed since the last sync. The fork\n * side wins whole-file on these, so upstream's hunks are dropped: the regression class where\n * a pinned stylesheet misses new upstream utilities that synced components rely on.\n */\nexport function findProtectedBehind(files: AnalyzedFile[]): AnalyzedFile[] {\n // Generated output is rebuilt from the fork's own sources, so upstream's copy has nothing to adopt.\n return files.filter((file) => file.upstreamChanged && !isManagedFile(file.path) && !isGeneratedFile(file.path));\n}\n\n/**\n * Every protected file upstream changed since the last sync that the sync leaves as the fork has\n * it: those both sides changed ({@link findProtectedBehind}) and the ignored files only upstream\n * changed, added or deleted (`MergeResult.ignoredUpstreamChanges`). Analyze lists them as one\n * section, on the terminal and on the review page.\n */\nexport function findProtectedBehindUpstream(\n result: Pick<MergeResult, 'files' | 'ignoredUpstreamChanges'>,\n): AnalyzedFile[] {\n const ignoredOnly = new Set((result.ignoredUpstreamChanges ?? []).flatMap((group) => group.paths));\n return [...findProtectedBehind(result.files), ...result.files.filter((file) => ignoredOnly.has(file.path))];\n}\n\n/** Hint under the protected files an upgrade kept the fork version of. */\nconst PROTECTED_BEHIND_HINT =\n 'pinned/ignored files where upstream also changed since the last upgrade; the fork side wins on conflict, ' +\n 'so review what upstream changed and adopt what you need.';\n\n/** Icon by protection kind (⨂ ignored, ⨀ pinned) for protected-but-behind lines. */\nexport function protectionIcon(file: AnalyzedFile): string {\n return file.isIgnored ? statusConfig.ignored.icon : statusConfig.pinned.icon;\n}\n\n/** Per-file detail: how many lines upstream changed (dropped by the fork-wins resolution). */\nfunction formatUpstreamChangedLines(file: AnalyzedFile): string {\n if (file.upstreamChangedLines === undefined) return '';\n return pc.yellow(` · ${plural(file.upstreamChangedLines, 'line')} changed upstream`);\n}\n\nexport function printSyncComplete(result: MergeResult, options: { stagedBranch?: string } = {}): void {\n const updated = result.files.filter((file) => file.status === 'behind' || file.status === 'diverged').length;\n const diverged = result.files.filter((file) => file.status === 'diverged').length;\n const merged = result.autoMergedFiles?.length ?? Math.max(0, diverged - result.conflicts.length);\n const conflicts = result.conflicts.length;\n\n const counts = `${plural(updated, 'file')} updated, ${merged} auto-merged, ${plural(conflicts, 'conflict')}`;\n console.info();\n if (options.stagedBranch) {\n console.info(`${checkMark} upgrade merge staged on '${options.stagedBranch}'`);\n console.info(pc.dim(` ${counts}. committing next...`));\n } else if (conflicts > 0) {\n console.info(`${warningMark} upgrade merge staged with conflicts`);\n console.info(pc.dim(` ${counts}`));\n } else {\n console.info(`${checkMark} upgrade merge complete`);\n console.info(pc.dim(` ${counts}`));\n }\n\n printProtectedConflicts(result);\n console.info();\n}\n\n/**\n * List protected files the sync resolved to the fork side although upstream changed them\n * (`MergeResult.protectedConflicts`). Printed right when the drop happens so it is not\n * discovered weeks later as a styling or behavior regression. Silent when empty.\n */\nfunction printProtectedConflicts(result: MergeResult): void {\n const paths = result.protectedConflicts ?? [];\n if (paths.length === 0) return;\n\n const byPath = new Map(result.files.map((file) => [file.path, file]));\n console.info();\n console.info(\n `${warningMark} ${pc.yellow(\n `${plural(paths.length, 'protected file kept', 'protected files kept')} the fork version, dropping upstream changes:`,\n )}`,\n );\n for (const path of paths) {\n const file = byPath.get(path);\n const icon = file ? protectionIcon(file) : statusConfig.pinned.icon;\n console.info(` ${icon} ${path}${file ? formatUpstreamChangedLines(file) : ''}`);\n }\n console.info(pc.dim(` ${PROTECTED_BEHIND_HINT}`));\n}\n\n/** `1 file`, `2 files`: a count with its noun. */\nexport function plural(count: number, singular: string, pluralForm = `${singular}s`): string {\n return `${count} ${count === 1 ? singular : pluralForm}`;\n}\n\n/**\n * Ignored paths upstream changed since the last sync while the fork left them untouched\n * (`MergeResult.ignoredUpstreamChanges`). Ignored paths never sync, so this is where new config\n * keys or version bumps under e.g. `shared/config` surface. One line per `ignored` entry with a\n * file count. Silent when empty.\n */\nexport function printIgnoredUpstreamChanges(result: MergeResult): void {\n const groups = result.ignoredUpstreamChanges ?? [];\n if (groups.length === 0) return;\n\n const total = groups.reduce((count, group) => count + group.paths.length, 0);\n console.info();\n console.info(\n `${warningMark} ${pc.yellow(`ignored paths changed upstream · ${plural(total, 'file')} under ${plural(groups.length, 'entry', 'entries')}`)}`,\n );\n for (const group of groups) {\n const counts = [plural(group.paths.length, 'file')];\n if (group.added > 0) counts.push(`${group.added} new`);\n if (group.deleted > 0) counts.push(`${group.deleted} deleted`);\n console.info(` ${statusConfig.ignored.icon} ${group.entry} ${pc.dim(`· ${counts.join(', ')}`)}`);\n }\n console.info(\n pc.dim(' the fork left these untouched and ignored paths never merge: review and adopt what you need.'),\n );\n}\n\n/**\n * Upstream CLI config entries the fork config does not follow (`MergeResult.upstreamOverrides`):\n * `overrides` entries and `packageJsonKeys` upstream added that the fork lacks, and ones\n * upstream dropped that the fork still has. The CLI config is managed and never merges, so these\n * only reach the fork by hand. Silent when there is nothing to report; one dim line when upstream's\n * config was unreadable.\n */\nexport function printUpstreamOverrideChanges(result: Pick<MergeResult, 'upstreamOverrides'>): void {\n const report = result.upstreamOverrides;\n if (!report) return;\n\n console.info();\n if (report.kind === 'unreadable') {\n const side = report.side === 'base' ? 'the last upgrade' : 'the incoming ref';\n console.info(\n pc.dim(`upstream CLI config not compared: upstream ${CONFIG_FILE} is missing or unreadable at ${side}`),\n );\n return;\n }\n\n const lines: string[] = [];\n for (const list of ['pinned', 'ignored', 'packageJsonKeys'] as const) {\n for (const entry of report[list].added) lines.push(` ${pc.green('+')} ${list}: ${entry}`);\n for (const entry of report[list].removed) {\n lines.push(` ${pc.red('−')} ${list}: ${entry}${pc.dim(' · dropped upstream, still in your config')}`);\n }\n }\n console.info(\n `${warningMark} ${pc.yellow(`upstream changed its CLI config · ${plural(lines.length, 'entry', 'entries')} to review`)}`,\n );\n for (const line of lines) console.info(line);\n console.info(pc.dim(` ${CONFIG_FILE} never merges: add or drop these by hand where they fit your app.`));\n}\n\n/**\n * A pinned file whose fork content is byte-identical to the previous upstream (status\n * `behind`) never actually diverged: the pin is silently freezing it at the old upstream\n * version and dropping upstream's new changes. Because the fork copy equals the old\n * upstream, this produces no merge conflict and no type error, so it slips through unseen\n * (e.g. a pinned nav-config losing new upstream entries). These are the pins worth a look.\n * Managed files (package.json, the lockfile, the CLI config) are always pinned but stay out:\n * the package sync reconciles their keys, so the pin masks nothing there.\n */\nexport function findMaskingPins(files: AnalyzedFile[]): AnalyzedFile[] {\n return files.filter(\n (file) =>\n file.isPinned &&\n !isManagedFile(file.path) &&\n file.status === 'behind' &&\n file.existsInFork &&\n file.existsInUpstream,\n );\n}\n\n/**\n * Warn when pins are masking upstream changes (see `findMaskingPins`). Silent by design\n * when there is nothing to report, so it is safe to call unconditionally after a summary.\n */\nexport function printMaskingPinWarning(files: AnalyzedFile[]): void {\n const masking = findMaskingPins(files);\n if (masking.length === 0) return;\n\n console.info();\n console.info(\n `${warningMark} ${pc.yellow(\n `${plural(masking.length, 'pinned file matches', 'pinned files match')} the previous upstream but changed upstream:`,\n )}`,\n );\n console.info(pc.yellow(' the pin keeps the old fork copy and silently drops those upstream changes:'));\n for (const file of masking) {\n console.info(pc.dim(` ⨀ ${file.path}`));\n }\n console.info(\n pc.yellow(' if the fork never customized these, unpin them in cella/cella.config.ts to take upstream.'),\n );\n console.info();\n}\n\n/**\n * Print the post-engine report block shared by analyze and sync: the summary, then the upstream\n * changes the sync never brings in (ignored paths and upstream's own CLI config), and pins that\n * silently freeze a file at the old upstream (no conflict, no type error).\n */\nexport function printEngineReports(result: MergeResult, title: string): void {\n printSummary(result.summary, title);\n printIgnoredUpstreamChanges(result);\n printUpstreamOverrideChanges(result);\n printMaskingPinWarning(result.files);\n}\n","{\n \"name\": \"@cellajs/cli\",\n \"version\": \"0.6.5\",\n \"private\": false,\n \"license\": \"MIT\",\n \"description\": \"CLI for managing your monorepo and upgrading it with upstream Cella.\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/cellajs/cella-cli\"\n },\n \"homepage\": \"https://cellajs.com\",\n \"author\": \"CellaJS <info@cellajs.com>\",\n \"engines\": {\n \"node\": \"26.x\"\n },\n \"type\": \"module\",\n \"main\": \"./dist/index.js\",\n \"exports\": {\n \".\": \"./dist/index.js\",\n \"./config\": {\n \"types\": \"./dist/config.d.ts\",\n \"import\": \"./dist/config.js\"\n },\n \"./bench\": {\n \"types\": \"./dist/bench.d.ts\",\n \"import\": \"./dist/bench.js\"\n },\n \"./style\": {\n \"types\": \"./dist/style.d.ts\",\n \"import\": \"./dist/style.js\"\n },\n \"./drive\": {\n \"types\": \"./dist/drive.d.ts\",\n \"import\": \"./dist/drive.js\"\n },\n \"./a11y\": {\n \"types\": \"./dist/a11y.d.ts\",\n \"import\": \"./dist/a11y.js\"\n },\n \"./screenshots\": {\n \"types\": \"./dist/screenshots.d.ts\",\n \"import\": \"./dist/screenshots.js\"\n },\n \"./pentest\": {\n \"types\": \"./dist/pentest.d.ts\",\n \"import\": \"./dist/pentest.js\"\n },\n \"./package.json\": \"./package.json\"\n },\n \"files\": [\n \"dist\",\n \"index.js\",\n \"README.md\"\n ],\n \"bin\": {\n \"cella\": \"index.js\"\n },\n \"imports\": {\n \"#/*\": \"./src/*\"\n },\n \"scripts\": {\n \"start\": \"tsx index.ts\",\n \"clean\": \"rimraf ./dist\",\n \"build\": \"tsup\",\n \"test-build\": \"pnpm run build && node index.js --help\",\n \"prepublishOnly\": \"pnpm run test:release\",\n \"check\": \"pnpm ts && pnpm biome check --write .\",\n \"ts\": \"tsgo --pretty\",\n \"lint\": \"biome check .\",\n \"lint:fix\": \"biome check --write .\",\n \"test\": \"vitest run\",\n \"test:release\": \"pnpm run build && vitest run --config vitest.release.config.ts\",\n \"test:watch\": \"vitest\",\n \"prepare\": \"lefthook install\"\n },\n \"dependencies\": {\n \"@inquirer/core\": \"^12.0.3\",\n \"@inquirer/prompts\": \"^8.7.2\",\n \"@pierre/diffs\": \"^1.5.1\",\n \"axe-core\": \"^4.14.0\",\n \"commander\": \"^15.0.0\",\n \"ts-morph\": \"^28.0.0\",\n \"yaml\": \"^2.9.1\",\n \"zod\": \"^4.6.5\"\n },\n \"devDependencies\": {\n \"@biomejs/biome\": \"^2.5.15\",\n \"@commitlint/cli\": \"^21.2.3\",\n \"@commitlint/config-conventional\": \"^21.2.3\",\n \"@evilmartians/lefthook\": \"^2.1.15\",\n \"@tailwindcss/node\": \"^4.3.3\",\n \"@types/node\": \"26.6.3\",\n \"@typescript/native-preview\": \"7.0.0-dev.20260707.2\",\n \"playwright\": \"^1.64.1\",\n \"rimraf\": \"^6.1.3\",\n \"tailwindcss\": \"^4.3.3\",\n \"tsup\": \"^8.5.1\",\n \"tsx\": \"^4.23.15\",\n \"typescript\": \"6.0.3\",\n \"vitest\": \"^5.0.3\"\n },\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"keywords\": [\n \"cella\",\n \"cellajs\",\n \"cli\",\n \"sync\",\n \"template\"\n ],\n \"packageManager\": \"pnpm@11.28.7\",\n \"commitlint\": {\n \"extends\": [\n \"@commitlint/config-conventional\"\n ],\n \"rules\": {\n \"type-enum\": [\n 2,\n \"always\",\n [\n \"feat\",\n \"fix\",\n \"perf\",\n \"refactor\",\n \"revert\",\n \"docs\",\n \"chore\",\n \"build\",\n \"ci\",\n \"style\",\n \"test\"\n ]\n ],\n \"header-max-length\": [\n 2,\n \"always\",\n 120\n ],\n \"body-max-line-length\": [\n 0,\n \"always\",\n 200\n ],\n \"footer-max-line-length\": [\n 0,\n \"always\",\n 200\n ]\n }\n }\n}\n","/**\n * Git command helpers: typed wrappers around common git operations, plus the\n * sync-point tracking and merge-base recovery machinery.\n */\n\nimport { execFile } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { mkdir, readdir, rmdir, unlink } from 'node:fs/promises';\nimport { dirname, join, resolve } from 'node:path';\nimport { promisify } from 'node:util';\nimport type { CommitRangeEntry } from '../config/types';\nimport { getEnvSnapshot } from './env';\nimport { MANIFEST_FILE, parseSyncManifest, readManifestBase, type SyncManifest } from './manifest';\n\nconst execFileAsync = promisify(execFile);\n\n/** Maximum stdout buffer for subprocess output (50MB). */\nexport const MAX_BUFFER = 50 * 1024 * 1024;\n\n/**\n * Replace the userinfo part of URLs in text (embedded tokens or passwords) with `***`.\n * Keeps secrets in git remote URLs out of error messages (CWE-532).\n */\nfunction redactUrlSecrets(text: string): string {\n return text.replace(/https?:\\/\\/[^@\\s]+@/g, (match) => {\n const protocol = match.startsWith('https') ? 'https' : 'http';\n return `${protocol}://***@`;\n });\n}\n\n/** Options for git command execution */\ninterface GitCommandOptions {\n /** Skip editor prompts (for merge commits, etc.) */\n skipEditor?: boolean;\n /** Ignore command errors: a failed command returns an empty string. */\n ignoreErrors?: boolean;\n /** Additional environment variables for the git command */\n env?: Record<string, string>;\n}\n\n/** Executes a git command and returns trimmed stdout. */\nexport async function git(args: string[], cwd: string, options: GitCommandOptions = {}): Promise<string> {\n const env = {\n ...getEnvSnapshot(),\n ...(options.skipEditor ? { GIT_EDITOR: 'true' } : {}),\n ...options.env,\n };\n\n try {\n const { stdout } = await execFileAsync('git', args, { cwd, env, maxBuffer: MAX_BUFFER });\n return stdout.trim();\n } catch (error) {\n if (options.ignoreErrors) return '';\n if (error instanceof Error) {\n error.message = redactUrlSecrets(error.message);\n }\n throw error;\n }\n}\n\n/** The object `ref` resolves to (`rev-parse -q --verify`), or null when it does not resolve. */\nexport async function revParse(cwd: string, ref: string): Promise<string | null> {\n return (await git(['rev-parse', '-q', '--verify', ref], cwd, { ignoreErrors: true })) || null;\n}\n\nexport async function getCurrentBranch(cwd: string): Promise<string> {\n return git(['rev-parse', '--abbrev-ref', 'HEAD'], cwd);\n}\n\nexport async function switchBranch(cwd: string, branch: string): Promise<void> {\n await git(['switch', branch], cwd);\n}\n\n/**\n * Detach HEAD at a commit or ref (`git switch --detach`), holding no branch.\n */\nexport async function switchDetached(cwd: string, ref: string): Promise<void> {\n await git(['switch', '--detach', ref], cwd);\n}\n\n/**\n * Create and check out a new branch from a specific start point (branch/ref).\n *\n * `--no-track` keeps a remote-tracking start point (`origin/main`) from becoming the new\n * branch's upstream, where a bare `git push` could then land on the trunk.\n */\nexport async function createBranchFrom(cwd: string, branch: string, startPoint: string): Promise<void> {\n await git(['switch', '-c', branch, '--no-track', startPoint], cwd);\n}\n\n/** Whether a local branch exists. */\nexport async function branchExists(cwd: string, branch: string): Promise<boolean> {\n return (await revParse(cwd, `refs/heads/${branch}`)) !== null;\n}\n\n/**\n * The worktree that has `branch` checked out, or null when none has.\n *\n * Git refuses to switch to a branch another worktree has checked out (and to move its ref), so\n * callers that would switch to a shared branch such as the trunk check here first.\n */\nexport async function getBranchWorktree(cwd: string, branch: string): Promise<string | null> {\n const output = await git(['worktree', 'list', '--porcelain'], cwd, { ignoreErrors: true });\n let worktree: string | null = null;\n for (const line of output.split('\\n')) {\n if (line.startsWith('worktree ')) worktree = line.slice('worktree '.length);\n else if (line === `branch refs/heads/${branch}`) return worktree;\n }\n return null;\n}\n\n/**\n * Fast-forward a branch no worktree has checked out to `ref`, without checking it out.\n *\n * Fetching from the repository itself (`git fetch . <ref>:<branch>`) moves the branch ref only,\n * and git refuses when that is not a fast-forward or a worktree has the branch checked out.\n */\nexport async function fastForwardBranch(cwd: string, branch: string, ref: string): Promise<void> {\n await git(['fetch', '--quiet', '.', `${ref}:refs/heads/${branch}`], cwd);\n}\n\n/** Delete a local branch (force). No-op if it doesn't exist. */\nexport async function deleteBranch(cwd: string, branch: string): Promise<void> {\n await git(['branch', '-D', branch], cwd, { ignoreErrors: true });\n}\n\n/**\n * Fast-forward the current branch from its upstream tracking branch.\n * Best-effort: ignores errors (e.g. no origin, offline, or nothing to pull).\n */\nexport async function pullFastForward(cwd: string): Promise<void> {\n await git(['pull', '--ff-only'], cwd, { ignoreErrors: true, skipEditor: true });\n}\n\n/** How the current branch compares to its upstream tracking branch. */\ninterface UpstreamStatus {\n /** Upstream tracking ref (e.g. `origin/main`), or null when none is configured. */\n upstream: string | null;\n /** Commits the local branch has that its upstream doesn't. */\n ahead: number;\n /** Commits the upstream has that the local branch doesn't. */\n behind: number;\n}\n\n/**\n * A branch's upstream tracking ref (e.g. `origin/main`), or null when none is configured.\n * `branch` defaults to the current one.\n */\nexport async function getBranchUpstream(cwd: string, branch = ''): Promise<string | null> {\n const upstream = await git(['rev-parse', '--abbrev-ref', '--symbolic-full-name', `${branch}@{upstream}`], cwd, {\n ignoreErrors: true,\n });\n return upstream || null;\n}\n\n/**\n * Fetch a branch's upstream, then report how far ahead/behind the branch is.\n *\n * `branch` defaults to the current one. A named branch is compared by ref, without checking it\n * out, so this also works for a branch another worktree has checked out.\n *\n * The fetch is best-effort so an offline/no-remote run still returns whatever is already local.\n * Returns `upstream: null` when the branch has no tracking branch configured (local-only repo).\n */\nexport async function getUpstreamStatus(cwd: string, branch?: string): Promise<UpstreamStatus> {\n const local = branch ? `refs/heads/${branch}` : 'HEAD';\n const upstream = await getBranchUpstream(cwd, branch);\n if (!upstream) return { upstream: null, ahead: 0, behind: 0 };\n\n // Refresh the remote-tracking ref so the comparison reflects the remote's latest state.\n const remote = upstream.split('/')[0];\n await git(['fetch', remote], cwd, { ignoreErrors: true, skipEditor: true });\n\n const counts = await git(['rev-list', '--left-right', '--count', `${local}...${upstream}`], cwd, {\n ignoreErrors: true,\n });\n const [ahead, behind] = counts.split(/\\s+/).map((n) => Number.parseInt(n, 10) || 0);\n return { upstream, ahead: ahead ?? 0, behind: behind ?? 0 };\n}\n\n/**\n * Push a branch to a remote, setting upstream tracking.\n *\n * `forceWithLease` allows a rewritten branch (see `flattenBranch`) to replace the version\n * already on the remote, while still refusing to overwrite remote commits we haven't seen.\n */\nexport async function pushBranch(\n cwd: string,\n remote: string,\n branch: string,\n options: { forceWithLease?: boolean } = {},\n): Promise<void> {\n await git(['push', '-u', ...(options.forceWithLease ? ['--force-with-lease'] : []), remote, branch], cwd);\n}\n\n/**\n * List merge commits made on the current branch itself: commits with two parents on the\n * first-parent chain from HEAD down to where the branch forked off `baseRef` (newest first).\n *\n * `--first-parent` keeps the walk on the branch's own commits, so merge commits that merely\n * exist inside a merged-in history (e.g. upstream's internal merges) don't count.\n */\nexport async function listBranchMergeCommits(cwd: string, baseRef: string): Promise<string[]> {\n const out = await git(['rev-list', '--merges', '--first-parent', `${baseRef}..HEAD`], cwd, { ignoreErrors: true });\n return out ? out.split('\\n').filter(Boolean) : [];\n}\n\n/**\n * Rewrite the current branch as one commit with `message`, parented on the point where the\n * branch forked off `baseRef`. The tree (content) is kept exactly as-is; only the commit\n * graph changes, so the branch's diff against the base is untouched.\n *\n * Undoes a two-parent merge commit that was already made; {@link commitSquash} says why one is unwanted.\n */\nexport async function flattenBranch(cwd: string, baseRef: string, message: string): Promise<void> {\n const parent = await getMergeBase(cwd, baseRef, 'HEAD');\n const tree = await git(['rev-parse', 'HEAD^{tree}'], cwd);\n const commit = await git(['commit-tree', tree, '-p', parent, '-m', message], cwd);\n await git(['reset', '--hard', commit], cwd);\n}\n\n/**\n * Commit the staged changes as a single-parent commit, discarding any in-progress merge state.\n *\n * A normal commit with `MERGE_HEAD` present records a two-parent merge commit. Because the fork\n * doesn't share pushed ancestry with upstream (the local `git replace` graft that makes merges\n * incremental is never pushed), such a merge commit makes the PR list the upstream branch's\n * entire history. Removing the merge state first collapses the staged delta into one clean\n * commit, so the PR shows a single commit with the incremental diff.\n */\nexport async function commitSquash(cwd: string, message: string): Promise<void> {\n for (const file of ['MERGE_HEAD', 'MERGE_MSG', 'MERGE_MODE']) {\n const path = await gitPath(cwd, file);\n if (existsSync(path)) await unlink(path);\n }\n await git(['commit', '-m', message], cwd, { skipEditor: true });\n}\n\n/**\n * Absolute path of a file in the current worktree's git dir. In a linked worktree `.git` is a\n * pointer file and the git dir lives under the main checkout's `.git/worktrees/<name>`.\n */\nasync function gitPath(cwd: string, file: string): Promise<string> {\n return resolve(cwd, await git(['rev-parse', '--git-path', file], cwd));\n}\n\n/**\n * Stage every change in the working tree (`git add -A`), including untracked and deleted files.\n */\nexport async function stageAll(cwd: string): Promise<void> {\n await git(['add', '-A'], cwd);\n}\n\n/**\n * Whether a merge is currently in progress (MERGE_HEAD present), in a linked worktree too.\n */\nexport async function mergeInProgress(cwd: string): Promise<boolean> {\n return (await revParse(cwd, 'MERGE_HEAD')) !== null;\n}\n\n/** A trailing `(#1519)` that squash merges add to a commit subject: the PR number, and the subject without it. */\nexport function splitPrNumber(subject: string): { subject: string; pr?: number } {\n const match = /\\s*\\(#(\\d+)\\)\\s*$/.exec(subject);\n return match ? { subject: subject.slice(0, match.index), pr: Number(match[1]) } : { subject };\n}\n\nexport async function getShortSha(cwd: string, ref: string): Promise<string> {\n return git(['rev-parse', '--short', ref], cwd);\n}\n\nexport async function getWorkingTreeChangeCount(cwd: string): Promise<number> {\n const status = await git(['status', '--porcelain'], cwd);\n if (!status) return 0;\n return status.split('\\n').filter(Boolean).length;\n}\n\n/** Add the remote, or point an existing one at `url` (`remote add` fails when it exists). */\nexport async function ensureRemote(cwd: string, remoteName: string, url: string): Promise<void> {\n await git(['remote', 'add', remoteName, url], cwd, { ignoreErrors: true });\n await git(['remote', 'set-url', remoteName, url], cwd);\n}\n\nexport async function fetchRemote(cwd: string, remoteName: string): Promise<void> {\n await git(['fetch', remoteName], cwd);\n}\n\n/**\n * Fetch upstream tags into a remote-scoped namespace (`refs/<remoteName>/tags/*`),\n * never the local `refs/tags/*`. This keeps cella's release tags from\n * colliding with the fork's own release tags (forks run their own release-please).\n * `--no-tags` prevents git from also writing them into `refs/tags/*`.\n */\nexport async function fetchUpstreamTags(cwd: string, remoteName: string): Promise<void> {\n await git(['fetch', remoteName, '--no-tags', `+refs/tags/*:refs/${remoteName}/tags/*`], cwd);\n}\n\n/**\n * A template release tag is a version, with or without the `v` prefix (`0.18.1`, `v0.5.0`).\n * Component tags (`create-cella-0.2.3`) and other namespaces (`archive/pre-release`) start with a name.\n */\nconst releaseTagPattern = /^v?\\d[\\w.+-]*$/;\n\n/**\n * Resolve the latest upstream release tag (the highest version tag) from the\n * remote-scoped tag namespace. Returns the short tag name and its full ref, or null\n * when no release exists.\n */\nexport async function resolveLatestReleaseTag(\n cwd: string,\n remoteName: string,\n): Promise<{ tag: string; ref: string } | null> {\n const prefix = `refs/${remoteName}/tags/`;\n const output = await git(['for-each-ref', '--sort=-version:refname', '--format=%(refname)', prefix], cwd, {\n ignoreErrors: true,\n });\n if (!output) return null;\n for (const line of output.split('\\n')) {\n const ref = line.trim();\n if (!ref) continue;\n const tag = ref.slice(prefix.length);\n if (releaseTagPattern.test(tag)) return { tag, ref };\n }\n return null;\n}\n\n/**\n * Resolve a ref named for upstream (`--ref`) to a commit: a branch on the upstream remote, then a\n * tag from the remote-scoped tag namespace, then the ref as given (a sha). Upstream names come\n * first, so `main` means upstream's main and `1.0.0` upstream's release, never the fork's own.\n * Revision suffixes work too (`main~2`). Returns the commit sha, plus the release tag when the\n * ref is a release tag name itself (`1.2.3`, not `1.2.3~1`); null when nothing resolves.\n */\nexport async function resolveUpstreamCommit(\n cwd: string,\n remoteName: string,\n ref: string,\n): Promise<{ sha: string; release?: { tag: string; ref: string } } | null> {\n if (ref.startsWith('-')) return null;\n const tagRef = `refs/${remoteName}/tags/${ref}`;\n for (const candidate of [`refs/remotes/${remoteName}/${ref}`, tagRef, ref]) {\n const sha = await revParse(cwd, `${candidate}^{commit}`);\n if (!sha) continue;\n const release = candidate === tagRef && releaseTagPattern.test(ref) ? { tag: ref, ref: tagRef } : undefined;\n return { sha, release };\n }\n return null;\n}\n\n/**\n * Whether upstream published `sha`: reachable from the upstream branch (`branchRef`, e.g.\n * `cella-upstream/main`) or from an upstream release tag (remote-scoped namespace).\n */\nexport async function isPublishedUpstream(\n cwd: string,\n remoteName: string,\n branchRef: string,\n sha: string,\n): Promise<boolean> {\n const containing = await git(\n [\n 'for-each-ref',\n '--count=1',\n '--format=%(refname)',\n '--contains',\n sha,\n `refs/remotes/${branchRef}`,\n `refs/${remoteName}/tags/[0-9]*`,\n `refs/${remoteName}/tags/v[0-9]*`,\n ],\n cwd,\n { ignoreErrors: true },\n );\n return containing !== '';\n}\n\nexport async function getCommitInfo(cwd: string, ref: string): Promise<CommitRangeEntry> {\n const format = '%H%n%s%n%ar'; // hash, subject, relative date\n const output = await git(['log', '-1', `--format=${format}`, ref], cwd);\n const [hash, message, date] = output.split('\\n');\n return { hash, message, date };\n}\n\n/** The number of commits in `toRef` that are not in `fromRef`. */\nexport async function countCommitsBetween(cwd: string, fromRef: string, toRef: string): Promise<number> {\n const output = await git(['rev-list', '--count', `${fromRef}..${toRef}`], cwd);\n return Number.parseInt(output.trim(), 10) || 0;\n}\n\n/**\n * List commits in a ref range, oldest-first to match GitHub compare ordering. With `limit`, only the\n * newest `limit` commits (git applies `-n` before `--reverse`).\n */\nexport async function listCommitsBetween(\n cwd: string,\n fromRef: string,\n toRef: string,\n limit?: number,\n): Promise<CommitRangeEntry[]> {\n const args = ['log', '--format=%H%x1f%s%x1f%ar%x1f%ct', '--reverse', `${fromRef}..${toRef}`];\n if (limit) args.push(`-n${limit}`);\n\n const output = await git(args, cwd, { ignoreErrors: true });\n if (!output) return [];\n\n const commits: CommitRangeEntry[] = [];\n for (const line of output.split('\\n')) {\n if (!line) continue;\n const [hash, message, date, timestamp] = line.split('\\x1f');\n if (!hash || !message || !date) continue;\n commits.push({ hash, message, date, timestamp: Number(timestamp) || undefined });\n }\n return commits;\n}\n\nexport interface FileChangeInfo {\n date: string;\n hash: string;\n /** Unix epoch seconds of the change, for sorting */\n timestamp: number;\n}\n\n/**\n * Relative date and commit hash of the last change for every file in a range,\n * as filePath -> { date, hash, timestamp }; one `git log` call for the whole range.\n */\nexport async function getFileChangeInfo(\n cwd: string,\n fromRef: string,\n toRef: string,\n): Promise<Map<string, FileChangeInfo>> {\n const info = new Map<string, FileChangeInfo>();\n const output = await git(['log', '--name-only', '--format=%h %ct %ar', `${fromRef}..${toRef}`], cwd, {\n ignoreErrors: true,\n });\n if (!output) return info;\n\n let currentHash = '';\n let currentDate = '';\n let currentTimestamp = 0;\n for (const line of output.split('\\n')) {\n if (!line) continue;\n const match = line.match(/^([a-f0-9]{7,})\\s+(\\d+)\\s+(.+)$/);\n if (match) {\n currentHash = match[1];\n currentTimestamp = Number(match[2]);\n currentDate = match[3];\n } else if (currentHash && currentDate && !info.has(line)) {\n // git log lists newest first, so the first entry per path is its last change.\n info.set(line, { date: currentDate, hash: currentHash, timestamp: currentTimestamp });\n }\n }\n return info;\n}\n\n/** Whether the working tree is clean (no uncommitted changes). */\nexport async function isClean(cwd: string): Promise<boolean> {\n return (await getWorkingTreeChangeCount(cwd)) === 0;\n}\n\n/**\n * Perform a merge in the current directory, leaving it staged (--no-commit --no-edit).\n * Always uses --no-ff to prevent fast-forward, ensuring HEAD stays in place\n * and MERGE_HEAD is created for proper merge state tracking.\n */\nexport async function merge(cwd: string, ref: string): Promise<{ success: boolean; conflicts: string[] }> {\n try {\n await git(['merge', '--no-ff', '--no-commit', '--no-edit', ref], cwd, { skipEditor: true });\n return { success: true, conflicts: [] };\n } catch (error) {\n const conflicts = await getConflictedFiles(cwd);\n if (conflicts.length > 0) {\n return { success: false, conflicts };\n }\n throw error;\n }\n}\n\nexport async function getConflictedFiles(cwd: string): Promise<string[]> {\n const output = await git(['diff', '--name-only', '--diff-filter=U'], cwd, { ignoreErrors: true });\n if (!output) return [];\n return output.split('\\n').filter(Boolean);\n}\n\nexport async function mergeAbort(cwd: string): Promise<void> {\n await git(['merge', '--abort'], cwd, { ignoreErrors: true });\n}\n\nexport async function fileExistsAtRef(cwd: string, ref: string, filePath: string): Promise<boolean> {\n const result = await git(['ls-tree', '--name-only', ref, '--', filePath], cwd, { ignoreErrors: true });\n return result !== '';\n}\n\n/**\n * Check out paths from a ref into index and worktree. During a merge this also settles unmerged\n * (conflicted) paths on `ref`'s version, which `git restore --source` refuses to do.\n */\nexport async function checkoutFromRef(cwd: string, ref: string, paths: string[]): Promise<void> {\n await git(['checkout', ref, '--', ...paths], cwd);\n}\n\n/**\n * Restore a path from a ref into the working tree only, leaving the index untouched,\n * so adopted changes remain unstaged for review.\n */\nexport async function restoreWorktreeFromRef(cwd: string, ref: string, filePath: string): Promise<void> {\n await git(['restore', `--source=${ref}`, '--worktree', '--', filePath], cwd);\n}\n\n/**\n * Restore paths from a ref into the working tree only, in batches, leaving the index untouched.\n * Paths are taken literally, so a file name with glob characters (`[id].tsx`) restores that file.\n */\nexport async function batchRestoreWorktreeFromRef(cwd: string, ref: string, filePaths: string[]): Promise<void> {\n const batchSize = 200;\n for (let i = 0; i < filePaths.length; i += batchSize) {\n const batch = filePaths.slice(i, i + batchSize);\n await git(['--literal-pathspecs', 'restore', `--source=${ref}`, '--worktree', '--', ...batch], cwd);\n }\n}\n\n/**\n * Paths at `ref` whose working tree copy is byte-identical to it. Renames are not detected, so a\n * file the working tree moved away counts as changed at its old path.\n */\nexport async function listIdenticalToRef(cwd: string, ref: string): Promise<Set<string>> {\n const nulSeparated = (out: string) => out.split('\\0').filter(Boolean);\n const [tracked, changed] = await Promise.all([\n git(['ls-tree', '-r', '--name-only', '-z', ref], cwd),\n git(['diff', '--name-only', '--no-renames', '-z', ref, '--'], cwd),\n ]);\n const differs = new Set(nulSeparated(changed));\n return new Set(nulSeparated(tracked).filter((path) => !differs.has(path)));\n}\n\n/** Tracked paths under `paths` whose working tree copy differs from `ref` (renames not detected). */\nexport async function listChangedSinceRef(cwd: string, ref: string, paths: string[]): Promise<string[]> {\n const out = await git(['--literal-pathspecs', 'diff', '--name-only', '--no-renames', '-z', ref, '--', ...paths], cwd);\n return out.split('\\0').filter(Boolean);\n}\n\nexport async function getMergeBase(cwd: string, ref1: string, ref2: string): Promise<string> {\n return git(['merge-base', ref1, ref2], cwd);\n}\n\n/** File change status codes from git diff-tree */\ntype FileChangeStatus = 'A' | 'D' | 'M' | 'T' | 'R';\n\n/** File change info from git diff-tree */\ninterface FileChange {\n status: FileChangeStatus;\n baseHash: string | null;\n targetHash: string | null;\n /** For renames: the original path (before rename) */\n oldPath?: string;\n}\n\n/**\n * Files changed between two refs (`git diff-tree -r -M90%`), as filePath -> FileChange.\n * Status codes: A=added, D=deleted, M=modified, T=type-changed, R=renamed (90% similarity\n * threshold). A rename is keyed by its NEW path, with `oldPath` stored in the value.\n */\nexport async function getFileChanges(\n cwd: string,\n baseRef: string,\n targetRef: string,\n): Promise<Map<string, FileChange>> {\n const output = await git(['diff-tree', '-r', '-M90%', '--no-commit-id', baseRef, targetRef], cwd, {\n ignoreErrors: true,\n });\n const changes = new Map<string, FileChange>();\n\n // diff-tree prints the all-zero hash for a missing side (added/deleted files).\n const hashOrNull = (hash: string) => (hash === '0'.repeat(40) ? null : hash);\n\n for (const line of output.split('\\n')) {\n // `:oldmode newmode oldhash newhash status\\tpath`; a rename is `Rxx\\toldpath\\tnewpath`.\n const m = line.match(/^:\\d+ \\d+ ([a-f0-9]+) ([a-f0-9]+) ([ADMRT])\\d*\\t([^\\t]+)(?:\\t(.+))?$/);\n if (!m) continue;\n const status = m[3] as FileChangeStatus;\n changes.set(m[5] ?? m[4], {\n status,\n baseHash: hashOrNull(m[1]),\n targetHash: hashOrNull(m[2]),\n ...(status === 'R' ? { oldPath: m[4] } : {}),\n });\n }\n return changes;\n}\n\n/** Lines added/removed for one path (`null` for binary files, where git reports `-`). */\ninterface DiffStat {\n additions: number | null;\n deletions: number | null;\n}\n\n/**\n * Lines added/removed per file between two refs (`git diff --numstat`), optionally limited\n * to `paths`. Renames are not detected (`--no-renames`) so every entry is keyed by its plain\n * path, never git's `old => new` notation.\n */\nexport async function getDiffStat(\n cwd: string,\n fromRef: string,\n toRef: string,\n paths: string[] = [],\n): Promise<Map<string, DiffStat>> {\n const args = ['diff', '--numstat', '--no-renames', fromRef, toRef];\n if (paths.length > 0) args.push('--', ...paths);\n const out = await git(args, cwd, { ignoreErrors: true });\n\n const stat = new Map<string, DiffStat>();\n for (const line of out.split('\\n')) {\n if (!line.trim()) continue;\n const [a, d, ...rest] = line.split('\\t');\n const path = rest.join('\\t');\n if (!path) continue;\n stat.set(path, {\n additions: a === '-' ? null : Number(a),\n deletions: d === '-' ? null : Number(d),\n });\n }\n return stat;\n}\n\n/** All file hashes at a ref (one `ls-tree -r` call), as filePath -> hash. */\nexport async function getFileHashesAtRef(cwd: string, ref: string): Promise<Map<string, string>> {\n const output = await git(['ls-tree', '-r', ref], cwd);\n const hashes = new Map<string, string>();\n\n if (!output) return hashes;\n\n for (const line of output.split('\\n')) {\n if (!line) continue;\n // Parse: <mode> <type> <hash>\\t<path>\n const match = line.match(/^\\d+ \\w+ ([a-f0-9]+)\\t(.+)$/);\n if (match) {\n const [, hash, filePath] = match;\n hashes.set(filePath, hash);\n }\n }\n\n return hashes;\n}\n\n/**\n * Tracked delete (`git rm -f`), recording a deletion in the merge result. With `cached`, the paths\n * leave the index only and the working tree is untouched. Best-effort: never throws.\n */\nexport async function gitRm(cwd: string, paths: string[], options: { cached?: boolean } = {}): Promise<void> {\n await git(['rm', ...(options.cached ? ['--cached'] : []), '-f', '--', ...paths], cwd, { ignoreErrors: true });\n}\n\n/** Files staged as new additions vs HEAD (diff-filter=A): absent at HEAD, brought in by a merge. */\nexport async function getStagedNewFiles(cwd: string): Promise<string[]> {\n const output = await git(['diff', '--cached', '--name-only', '--diff-filter=A'], cwd, { ignoreErrors: true });\n return output ? output.split('\\n').filter(Boolean) : [];\n}\n\n/** Move a file (`git mv -f`), creating parent directories as needed. */\nexport async function gitMv(cwd: string, oldPath: string, newPath: string): Promise<void> {\n const newDir = dirname(join(cwd, newPath));\n await mkdir(newDir, { recursive: true });\n\n await git(['mv', '-f', oldPath, newPath], cwd);\n}\n\nexport async function fileExistsInWorktree(cwd: string, filePath: string): Promise<boolean> {\n return existsSync(join(cwd, filePath));\n}\n\n/**\n * Remove a file from the worktree filesystem if it exists.\n * Used to clean up files that git rm may not have removed (e.g., after squash merge).\n */\nexport async function removeFileFromWorktree(cwd: string, filePath: string): Promise<void> {\n const fullPath = join(cwd, filePath);\n try {\n await unlink(fullPath);\n await cleanupEmptyParentDirs(cwd, dirname(filePath));\n } catch {\n // File missing or not removable: ignore\n }\n}\n\n/**\n * Fully remove a file: tracked delete from the index (git rm) plus filesystem removal\n * for cases git rm leaves behind (e.g. during a merge), including empty parent dirs.\n */\nexport async function removeFileFully(cwd: string, filePath: string): Promise<void> {\n await gitRm(cwd, [filePath]);\n await removeFileFromWorktree(cwd, filePath);\n}\n\n/**\n * Recursively remove empty parent directories up to (but not including) the root.\n * Stops when encountering a non-empty directory or the root.\n */\nasync function cleanupEmptyParentDirs(cwd: string, relativePath: string): Promise<void> {\n if (!relativePath || relativePath === '.') return;\n\n const fullPath = join(cwd, relativePath);\n try {\n const entries = await readdir(fullPath);\n if (entries.length === 0) {\n await rmdir(fullPath);\n await cleanupEmptyParentDirs(cwd, dirname(relativePath));\n }\n } catch {\n // Directory missing, not empty, or not removable: stop\n }\n}\n\n// Sync ref tracking and merge-base recovery.\n\n/** The local ref holding the upstream commit of the last upgrade, and `<ref>-head` the fork HEAD then. */\nconst LAST_UPGRADE_REF = 'refs/cella/last-upgrade';\n\n/** Where CLI 0.5 kept the same two refs; read while a fork has not upgraded under 0.6. */\nconst LEGACY_LAST_UPGRADE_REF = 'refs/cella/last-sync';\n\n/**\n * Store the upstream commit hash after a successful upgrade merge.\n * Used to recover correct merge-base when squash strategy creates single-parent commits.\n *\n * Also records the fork HEAD at store time as `refs/cella/last-upgrade-head`. The last-upgrade\n * ref is written while the merge is still staged (before the user commits), so it cannot be\n * trusted on its own: if the user runs `git merge --abort`, HEAD never advances and the ref\n * is left pointing at an upstream commit that was never actually integrated. Recording HEAD\n * lets `getEffectiveMergeBase` detect that an aborted/uncommitted merge produced a stale ref.\n */\nexport async function storeLastSyncRef(cwd: string, upstreamHash: string): Promise<void> {\n await git(['update-ref', LAST_UPGRADE_REF, upstreamHash], cwd);\n const head = await revParse(cwd, 'HEAD');\n if (head) await git(['update-ref', `${LAST_UPGRADE_REF}-head`, head], cwd);\n}\n\n/** Stage a single path (git add). Best-effort: never throws. */\nexport async function stagePath(cwd: string, path: string): Promise<void> {\n await git(['add', '--', path], cwd, { ignoreErrors: true });\n}\n\n/** The stored last-upgrade ref and its HEAD, under the 0.5 names when the 0.6 ones are absent. */\nasync function readStoredSyncRefs(cwd: string): Promise<{ ref: string; head: string | null } | null> {\n for (const name of [LAST_UPGRADE_REF, LEGACY_LAST_UPGRADE_REF]) {\n const ref = await revParse(cwd, name);\n if (ref) return { ref, head: await revParse(cwd, `${name}-head`) };\n }\n return null;\n}\n\n/** The stored last-upgrade upstream ref, or null when no upgrade has been recorded. */\nexport async function getStoredSyncRef(cwd: string): Promise<string | null> {\n return (await readStoredSyncRefs(cwd))?.ref ?? null;\n}\n\n/**\n * Read the sync manifest committed at a specific ref.\n *\n * This intentionally reads from git, not the working tree: a staged/aborted sync can leave the\n * worktree manifest ahead of the committed trunk, and that must not validate a sync point.\n */\nexport async function readManifestAtRef(cwd: string, ref: string): Promise<SyncManifest | null> {\n const raw = await git(['show', `${ref}:${MANIFEST_FILE}`], cwd, { ignoreErrors: true });\n if (!raw) return null;\n return parseSyncManifest(raw);\n}\n\n/** The upstream base commit SHA from the manifest committed at `ref`, or null. */\nexport async function readManifestBaseAtRef(cwd: string, ref: string): Promise<string | null> {\n const manifest = await readManifestAtRef(cwd, ref);\n return manifest ? manifest.upstream.commit.toLowerCase() : null;\n}\n\n/**\n * Read the `version` field of the root `package.json` committed at a ref.\n * Returns null when the file is absent or unparsable.\n */\nexport async function readPackageVersionAtRef(cwd: string, ref: string): Promise<string | null> {\n const raw = await git(['show', `${ref}:package.json`], cwd, { ignoreErrors: true });\n if (!raw) return null;\n\n try {\n const version = (JSON.parse(raw) as { version?: unknown }).version;\n return typeof version === 'string' && version ? version : null;\n } catch {\n return null;\n }\n}\n\n/** Whether a commit object is present in the local object store. */\nasync function commitObjectExists(cwd: string, sha: string): Promise<boolean> {\n return (await revParse(cwd, `${sha}^{commit}`)) !== null;\n}\n\n/** The root (parentless) commit of a ref's history, such as a create-cella scaffold's \"Initial commit\", or null. */\nasync function getRootCommit(cwd: string, ref: string): Promise<string | null> {\n const out = await git(['rev-list', '--max-parents=0', ref], cwd, { ignoreErrors: true });\n const roots = out.split('\\n').filter(Boolean);\n return roots.length > 0 ? roots[roots.length - 1] : null;\n}\n\n/**\n * Read the scaffold provenance trailer from the fork's root commit message.\n *\n * create-cella stamps `Cella-Base: <sha>` on the initial commit, recording the exact\n * upstream commit the scaffold snapshot was taken from. Riding in commit history, it\n * travels with push/clone (a local ref does not) and needs no file in the worktree.\n */\nasync function readRootTrailerBase(cwd: string, headRef: string): Promise<string | null> {\n const root = await getRootCommit(cwd, headRef);\n if (!root) return null;\n const message = await git(['show', '-s', '--format=%B', root], cwd, { ignoreErrors: true });\n const match = message.match(/^cella-base:\\s*([0-9a-f]{40})$/im);\n return match ? match[1].toLowerCase() : null;\n}\n\n/**\n * Whether the repository is a shallow clone (created with `git clone --depth`, or otherwise\n * carrying a `.git/shallow` graft). Shallow history is truncated below the graft point, so the\n * real common ancestor with upstream is absent: `git merge-base` finds nothing even when one\n * exists in the full history, and the 3-way file analysis walks an incomplete commit graph.\n */\nasync function isShallowRepository(cwd: string): Promise<boolean> {\n const out = await git(['rev-parse', '--is-shallow-repository'], cwd, { ignoreErrors: true });\n return out === 'true';\n}\n\n/**\n * Restore full history on a shallow clone by fetching from its own origin, so merge-base and the\n * 3-way analysis see the real commit graph.\n *\n * Throws an actionable error when the history cannot be restored (e.g. no reachable origin),\n * because an upgrade cannot run correctly against truncated history.\n */\nasync function unshallowRepository(cwd: string): Promise<void> {\n try {\n await git(['fetch', '--unshallow'], cwd, { skipEditor: true });\n } catch (error) {\n const detail = error instanceof Error ? `\\n\\n${error.message}` : '';\n throw new Error(\n 'this is a shallow clone and its full history could not be restored automatically.\\n' +\n 'An upgrade needs the complete history to find the common ancestor with upstream. Restore it with:\\n\\n' +\n ' git fetch --unshallow\\n\\n' +\n `then re-run the upgrade.${detail}`,\n );\n }\n}\n\n/**\n * Ensure a native git merge-base exists between the fork and upstream.\n *\n * A create-cella scaffold has unrelated history with upstream, so `git merge-base` finds nothing\n * and every upgrade fails at the very first step. This bootstraps ancestry non-destructively:\n * 0. If the repo is a shallow clone, restore its full history first: the real ancestor is\n * truncated away, so no graft can recover it; deepening the history brings it back.\n * 1. If a native merge-base already exists, do nothing (the common case).\n * 2. Otherwise resolve the logical base commit: sources are tried in order and the first\n * one whose commit actually exists after the upstream fetch wins: the sync-point\n * record (`refs/cella/last-upgrade` ref, then committed/worktree `cella/cella.manifest.json`),\n * then the scaffold origin (the root commit's `Cella-Base:` trailer).\n * 3. Graft the fork's root commit onto that base with `git replace --graft`, so every native\n * git operation (merge-base and the 3-way merge itself) sees correct ancestry, and record\n * the base as `refs/cella/last-upgrade`. The replace ref is local-only and never pushed, so\n * the fork's own published history stays clean.\n *\n * Throws an actionable error when no valid base can be determined.\n */\nexport async function ensureSyncBase(cwd: string, headRef: string, upstreamRef: string): Promise<void> {\n if (await isShallowRepository(cwd)) {\n await unshallowRepository(cwd);\n }\n\n const nativeBase = await git(['merge-base', headRef, upstreamRef], cwd, { ignoreErrors: true });\n if (nativeBase) return;\n\n const sources: Array<() => Promise<string | null>> = [\n () => getStoredSyncRef(cwd),\n () => readManifestBaseAtRef(cwd, headRef),\n () => readManifestBase(cwd),\n () => readRootTrailerBase(cwd, headRef),\n ];\n\n let baseSha: string | null = null;\n for (const source of sources) {\n const sha = await source();\n if (sha && (await commitObjectExists(cwd, sha))) {\n baseSha = sha;\n break;\n }\n }\n if (!baseSha) {\n throw new Error(\n `no common ancestor between the fork and '${upstreamRef}', and no upgrade base could be determined.\\n\\n` +\n 'This fork has unrelated history with upstream (typical for a create-cella scaffold, or after\\n' +\n 'upstream squashed its history) and no recorded base commit exists after fetching\\n' +\n 'upstream. Seed it manually with the upstream commit the fork was created from, then re-run the upgrade:\\n\\n' +\n ` git update-ref ${LAST_UPGRADE_REF} <upstream-sha>\\n`,\n );\n }\n\n const root = await getRootCommit(cwd, headRef);\n if (!root) throw new Error(\"could not determine the fork's root commit to bootstrap upgrade ancestry\");\n\n await git(['replace', '-f', '--graft', root, baseSha], cwd);\n await git(['update-ref', LAST_UPGRADE_REF, baseSha], cwd);\n}\n\n/** Whether `ancestor` is an ancestor of `descendant`. */\nexport async function isAncestor(cwd: string, ancestor: string, descendant: string): Promise<boolean> {\n try {\n await git(['merge-base', '--is-ancestor', ancestor, descendant], cwd);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Legacy sync point for forks that last upgraded before the manifest was committed: the local\n * last-upgrade ref, discarded while HEAD still sits at the recorded HEAD (an aborted merge,\n * see {@link storeLastSyncRef}).\n */\nasync function getLegacySyncRef(cwd: string, headRef: string): Promise<string | null> {\n const stored = await readStoredSyncRefs(cwd);\n if (!stored) return null;\n\n if (stored.head) {\n const currentHead = await revParse(cwd, headRef);\n if (currentHead && currentHead === stored.head) return null;\n }\n\n return stored.ref;\n}\n\n/**\n * The effective merge-base, preferring the recorded sync point when it's more recent.\n * This handles stale merge-base caused by previous squash syncs (single-parent commits\n * don't update git's merge-base graph).\n *\n * The manifest committed at `headRef` is the authoritative record: it lands iff the sync commit lands\n * and travels with the repo. The local last-upgrade ref is the fallback for forks whose last upgrade predates it.\n *\n * @returns The effective base commit, whether git's base was stale, and the recorded sync point\n */\nexport async function getEffectiveMergeBase(\n cwd: string,\n headRef: string,\n upstreamRef: string,\n): Promise<{ base: string; isStale: boolean; storedRef: string | null }> {\n const gitBase = await getMergeBase(cwd, headRef, upstreamRef);\n\n const recorded = (await readManifestBaseAtRef(cwd, headRef)) ?? (await getLegacySyncRef(cwd, headRef));\n if (!recorded) {\n return { base: gitBase, isStale: false, storedRef: null };\n }\n\n // Only prefer the recorded sync point when it is actually newer than git's own answer.\n const recordedIsNewer = await isAncestor(cwd, gitBase, recorded);\n if (recordedIsNewer && recorded !== gitBase) {\n return { base: recorded, isStale: true, storedRef: recorded };\n }\n\n return { base: gitBase, isStale: false, storedRef: recorded };\n}\n\n/**\n * Run `fn` with a temporary local graft that makes `baseSha` an ancestor of `headRef`, so\n * native git operations (merge-base resolution and the 3-way merge itself) see the recorded\n * sync point, not a stale historical ancestor.\n *\n * Squash syncs advance the recorded sync point (manifest + `refs/cella/last-upgrade`) without\n * advancing git's commit graph, so a plain `git merge` replays every upstream change since the\n * last true merge ancestor: hunks the fork already integrated (or deliberately resolved away)\n * re-apply as clean auto-merges or re-conflict on every sync. The graft adds `baseSha` as an\n * extra parent of the `headRef` commit via `git replace --graft`; replace refs are local-only\n * and the ref is deleted right after `fn`, so nothing ever leaks into pushed history.\n *\n * Runs `fn` without grafting when the graft is unnecessary or unsafe:\n * - `baseSha` is already an ancestor of `headRef` (git's own merge-base is not stale), or\n * - a replacement object already exists for the `headRef` commit (never clobber, e.g. the\n * scaffold root graft from {@link ensureSyncBase} when the root commit is HEAD).\n */\nexport async function withTemporarySyncBaseGraft<T>(\n cwd: string,\n headRef: string,\n baseSha: string,\n fn: () => Promise<T>,\n): Promise<T> {\n const headSha = await revParse(cwd, headRef);\n if (!headSha || !(await commitObjectExists(cwd, baseSha))) return fn();\n\n // Native ancestry already covers the base: nothing to graft.\n if (await isAncestor(cwd, baseSha, headSha)) return fn();\n\n // Never clobber an existing replacement object for this commit.\n const existingReplacement = await git(['replace', '-l', headSha], cwd, { ignoreErrors: true });\n if (existingReplacement) return fn();\n\n const parentsRaw = await git(['log', '-1', '--format=%P', headSha], cwd, { ignoreErrors: true });\n const parents = parentsRaw ? parentsRaw.split(/\\s+/) : [];\n\n await git(['replace', '-f', '--graft', headSha, ...parents, baseSha], cwd);\n try {\n return await fn();\n } finally {\n await git(['replace', '-d', headSha], cwd, { ignoreErrors: true });\n }\n}\n","/**\n * Cella upgrade manifest: the committed record of the last upstream point the app upgraded to.\n * `upstream.commit` seeds the merge-base on a clone without `refs/cella/last-upgrade` (fresh clone,\n * CI), and the rest shows in each upgrade PR which upstream point the app moved to. The merge engine\n * stages it with every applied upgrade.\n */\n\nimport { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\n\n/** Committed manifest path, relative to the fork repo root. */\nexport const MANIFEST_FILE = 'cella/cella.manifest.json';\n\n/** The last upstream sync point recorded in {@link MANIFEST_FILE}. */\nexport interface SyncManifest {\n upstream: {\n /** GitHub slug of the upstream repo, e.g. 'cellajs/cella'. */\n repo?: string;\n /** Tracking mode at upgrade time. */\n track?: 'release' | 'branch';\n /** Integrated upstream commit SHA: the merge-base bootstrap seed. */\n commit: string;\n /** Release tag when tracking releases, else null. */\n release?: string | null;\n /** Human-readable link to the commit or release on GitHub. */\n url?: string;\n /** UTC timestamp of the upgrade (ISO 8601). */\n upgradedAt?: string;\n };\n}\n\n/** Parse and validate raw manifest JSON. Returns null when malformed or missing a 40-hex upstream commit. */\nexport function parseSyncManifest(raw: string): SyncManifest | null {\n try {\n const parsed = JSON.parse(raw) as SyncManifest;\n const commit = parsed?.upstream?.commit;\n if (typeof commit !== 'string' || !/^[0-9a-f]{40}$/i.test(commit)) return null;\n return parsed;\n } catch {\n return null;\n }\n}\n\n/** Read and validate the manifest. Returns null when absent or malformed. */\nexport async function readSyncManifest(cwd: string): Promise<SyncManifest | null> {\n try {\n return parseSyncManifest(await readFile(join(cwd, MANIFEST_FILE), 'utf8'));\n } catch {\n return null;\n }\n}\n\n/**\n * Return the recorded upstream base commit SHA, or null.\n * Bootstrap seed for {@link ensureSyncBase} when no local `refs/cella/last-upgrade` exists.\n */\nexport async function readManifestBase(cwd: string): Promise<string | null> {\n const manifest = await readSyncManifest(cwd);\n return manifest ? manifest.upstream.commit.toLowerCase() : null;\n}\n\n/** Write the manifest as pretty JSON with a trailing newline. */\nexport async function writeSyncManifest(cwd: string, manifest: SyncManifest): Promise<void> {\n const manifestPath = join(cwd, MANIFEST_FILE);\n // Ensure the cella/ folder exists (a fresh fork may not have it yet on its first upgrade).\n await mkdir(dirname(manifestPath), { recursive: true });\n await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\\n`, 'utf8');\n}\n","/**\n * Error helpers for sync CLI.\n */\n\n/** The message of an unknown thrown value: an Error's message, anything else via String(). */\nexport function errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n","import { mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport process from 'node:process';\n\nexport interface LockInfo {\n pid: number;\n scenario: string;\n startedAt: string;\n target: string;\n}\n\nconst isAlive = (pid: number) => {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n return (error as NodeJS.ErrnoException).code === 'EPERM';\n }\n};\n\nconst readLock = (file: string): LockInfo | null => {\n try {\n return JSON.parse(readFileSync(file, 'utf8')) as LockInfo;\n } catch {\n return null;\n }\n};\n\nexport interface Lock {\n /** Names the scenario now running, for a run that waits on this one. */\n update(scenario: string): void;\n release(): void;\n /** Bench runs of this clone that target another stack: they share the machine. */\n others: LockInfo[];\n}\n\n/**\n * Claims the stack at `target` for this process, so two runs never measure one stack at once. A lock whose process\n * has exited is taken over. Released on exit.\n * @returns The lock, or the live run that holds it.\n */\nexport function acquireLock(dataDir: string, target: string, scenario: string): { lock: Lock } | { holder: LockInfo } {\n const dir = join(dataDir, 'locks');\n mkdirSync(dir, { recursive: true });\n const { host } = new URL(target);\n const file = join(dir, `${host.replace(/[^a-z0-9.-]/gi, '_')}.json`);\n const info: LockInfo = { pid: process.pid, scenario, startedAt: new Date().toISOString(), target };\n\n for (let attempt = 0; ; attempt++) {\n try {\n writeFileSync(file, JSON.stringify(info), { flag: 'wx' });\n break;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'EEXIST' || attempt > 2) throw error;\n const holder = readLock(file);\n if (holder && isAlive(holder.pid)) return { holder };\n try {\n unlinkSync(file);\n } catch {}\n }\n }\n\n const release = () => {\n if (readLock(file)?.pid !== process.pid) return;\n try {\n unlinkSync(file);\n } catch {}\n };\n process.once('exit', release);\n const others = readdirSync(dir)\n .map((name) => join(dir, name))\n .filter((path) => path !== file)\n .flatMap((path) => readLock(path) ?? [])\n .filter((other) => isAlive(other.pid));\n\n return {\n lock: {\n update: (name) => writeFileSync(file, JSON.stringify({ ...info, scenario: name })),\n release,\n others,\n },\n };\n}\n","/** The variable that hands the app's bench entry what `pnpm cella bench` asks of it, as JSON. */\nexport const BENCH_REQUEST_ENV = 'CELLA_BENCH';\n\n/** What `pnpm cella bench` asks of the app's bench entry. */\nexport interface BenchRequest {\n /** Scenario names in run order; empty to pick one from a list. */\n scenarios: string[];\n /** Every scenario in turn: one line each, a cooldown between full runs and a table at the end. */\n all: boolean;\n short: boolean;\n skipSeed: boolean;\n /** Absolute path of the folder whose `<name>.ts` modules are the scenarios. */\n scenariosDir: string;\n /** Holds `baselines/`, `logs/` and `locks/`. */\n dataDir: string;\n}\n"],"mappings":"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","names":["mkdirSync","join","performance","process","process","process","process","stripVTControlCharacters","process","stripVTControlCharacters","process","pct","stats","resolve","ms","stats","performance","count","seconds","resolve","performance","stats","performance","join","mkdirSync","process","performance","resolve","counters","seconds","http","readdirSync","join","process","resolve","process","existsSync","mkdir","dirname","join","join","process","mkdirSync","readdirSync","readFileSync","writeFileSync","join","process","join","readdirSync","process"]}
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/config/types.ts"],"sourcesContent":["/**\n * Configuration types: the public cella/cella.config.ts surface, its runtime\n * schema, and the internal types the services share.\n */\n\nimport { z } from 'zod';\n\n// Public API for cella/cella.config.ts.\n\nconst PACKAGE_JSON_KEYS = [\n 'dependencies',\n 'devDependencies',\n 'peerDependencies',\n 'optionalDependencies',\n 'scripts',\n 'engines',\n 'packageManager',\n 'overrides',\n 'exports',\n 'pnpm',\n] as const;\n\n/** Valid package.json keys that can merge from upstream */\nexport type PackageJsonKey = (typeof PACKAGE_JSON_KEYS)[number];\n\n/**\n * Settings: all configurable options for the cella CLI.\n * Hover over each property for documentation.\n */\nexport interface SyncSettings {\n /** Upstream repository URL (e.g., 'git@github.com:cellajs/cella.git') */\n upstreamUrl: string;\n\n /**\n * Upstream branch, used for `upstreamTrack: 'branch'` and for GitHub file links.\n * Defaults to 'main'.\n */\n upstreamBranch?: string;\n\n /**\n * How to track upstream cella. Defaults to 'release'.\n * - 'release' (default): upgrade to a published cella release tag (`0.18.1`, with or without `v`). Stable and\n * reviewable: each bump maps to a changelog. Uses the latest release.\n * - 'branch': follow the bleeding-edge tip of `upstreamBranch`. For cella\n * maintainers and forks doing active development on top of unreleased changes.\n */\n upstreamTrack?: 'release' | 'branch';\n\n /**\n * Trunk branch that `pnpm cella upgrade` cuts the temporary branch from and that you open the\n * squash-merge PR into. Defaults to 'main'. Cutting fresh from trunk each cycle keeps every\n * PR limited to that cycle's upstream delta (no accumulation) and `main` linear.\n */\n releaseBase?: string;\n\n /** Which package.json keys merge from upstream (default: ['dependencies', 'devDependencies']). `type` always merges. */\n packageJsonKeys?: PackageJsonKey[];\n\n /** Merge the `packageJsonKeys` of every package.json after the upgrade merge (default: true). */\n mergePackageJson?: boolean;\n\n /** @deprecated Renamed to `packageJsonKeys`. */\n packageJsonSync?: PackageJsonKey[];\n\n /** @deprecated Renamed to `mergePackageJson`. */\n syncWithPackages?: boolean;\n\n /**\n * How to link files in CLI output.\n * - 'commit' (default): Link to the commit that changed the file on GitHub.\n * - 'file': Link to the file in the repo at the upstream branch on GitHub.\n */\n fileLinkMode?: 'commit' | 'file';\n}\n\n/**\n * Configuration for a downstream fork repository that cella interacts with.\n *\n * Cella is the upstream/source-of-truth. For each fork it can:\n * - pull contributions FROM the fork's `pullBranch` (contributions service)\n * - run the normal temporary-branch upgrade flow inside the fork (forks service)\n */\nexport interface ForkConfig {\n /** Display name for the fork (also used as the contrib/<name> branch slug) */\n name: string;\n /** Local path to the fork repository (relative to this config or absolute) */\n localPath: string;\n /**\n * Git remote URL of the fork (e.g. 'git@github.com:org/fork.git').\n * When set, the contributions service fetches the fork's `pullBranch` from this\n * remote (the authoritative committed ref), so the comparison does not depend on\n * the local checkout being up to date. The local clone is still used to read the\n * fork's owned-folder territory when available.\n */\n remoteUrl?: string;\n /** Fork branch that cella pulls contributions from (contributions service) */\n pullBranch: string;\n}\n\n/** User-configurable sync options for cella/cella.config.ts. */\nexport interface CellaCliConfig {\n /** Core sync settings */\n settings: SyncSettings;\n\n /**\n * File overrides for controlling sync behavior per folder/file.\n */\n overrides?: {\n /**\n * Paths the fork fully owns: never synced (existing or new).\n * Exact paths or directory prefixes, not globs: 'bench' matches 'bench/' and\n * everything under it, 'README.md' matches that exact file.\n * Local territory: upstream cannot add, modify, or delete anything under these.\n */\n ignored?: string[];\n\n /**\n * Paths pinned to the fork: the fork copy wins whole-file.\n * Exact paths or directory prefixes, not globs: 'bench' matches 'bench/' and\n * everything under it, 'README.md' matches that exact file.\n * Upstream changes to a pinned file are dropped, not merged: analyze lists them under\n * \"protected but behind upstream\" to adopt by hand.\n * If fork deleted a pinned file, deletion is respected.\n */\n pinned?: string[];\n };\n\n /**\n * Downstream fork repositories that cella interacts with (for upstream template repos).\n * When configured, the 'forks' and 'contributions' services appear in the CLI menu.\n */\n forks?: ForkConfig[];\n}\n\n/**\n * Helper function for defining cella/cella.config.ts with type checking.\n */\nexport function defineConfig(config: CellaCliConfig): CellaCliConfig {\n return config;\n}\n\n/**\n * Strict runtime schema mirroring {@link CellaCliConfig}.\n *\n * The TS interfaces above stay the source of truth (and keep their hover docs for\n * config authors). This schema is what's enforced at load time: `.strict()` rejects\n * unknown keys, so a typo like `ignoredFolders` for `ignored` fails closed; a silent\n * fallback would drop sync protection (tsx does not typecheck the config).\n *\n * The `satisfies z.ZodType<CellaCliConfig>` assertion keeps schema and interface in\n * lockstep: if either drifts (a renamed/removed/mis-typed field), TypeScript errors here.\n */\nexport const cellaConfigSchema = z\n .object({\n settings: z\n .object({\n upstreamUrl: z.string().min(1),\n upstreamBranch: z.string().min(1).optional(),\n upstreamTrack: z.enum(['release', 'branch']).optional(),\n releaseBase: z.string().min(1).optional(),\n packageJsonKeys: z.array(z.enum(PACKAGE_JSON_KEYS)).optional(),\n mergePackageJson: z.boolean().optional(),\n packageJsonSync: z.array(z.enum(PACKAGE_JSON_KEYS)).optional(),\n syncWithPackages: z.boolean().optional(),\n fileLinkMode: z.enum(['commit', 'file']).optional(),\n })\n .strict(),\n overrides: z\n .object({\n ignored: z.array(z.string()).optional(),\n pinned: z.array(z.string()).optional(),\n })\n .strict()\n .optional(),\n forks: z\n .array(\n z\n .object({\n name: z.string().min(1),\n localPath: z.string().min(1),\n remoteUrl: z.string().optional(),\n pullBranch: z.string().min(1),\n })\n .strict(),\n )\n .optional(),\n })\n .strict() satisfies z.ZodType<CellaCliConfig>;\n\n// Internal types for the services.\n\n/** Sync services available in the CLI */\nexport type SyncService = 'analyze' | 'upgrade' | 'align' | 'audit' | 'forks' | 'contributions' | 'stats';\n\n/** Analyze output scope for interactive and machine-readable flows */\nexport type AnalyzeScope = 'all' | 'risk' | 'protected';\n\n/** Runtime configuration with all resolved values */\nexport interface RuntimeConfig extends CellaCliConfig {\n /** Resolved fork repository path */\n forkPath: string;\n\n /** Full upstream remote ref (e.g., 'cella-upstream/development') */\n upstreamRef: string;\n\n /** Selected sync service */\n service: SyncService;\n\n /** Non-interactive list output (for LLM/agent usage) */\n list: boolean;\n\n /** Machine-readable JSON output (for tooling/agent usage) */\n json: boolean;\n\n /** Scope of files returned in analyze list/json output */\n scope?: AnalyzeScope;\n\n /** Show verbose output */\n verbose: boolean;\n\n /** Pre-selected fork name (skips fork selection prompt) */\n fork?: string;\n\n /**\n * Override upstream tracking for this run (analyze/upgrade), ignoring `settings.upstreamTrack`.\n * 'branch' follows the upstream branch tip (bleeding edge); 'release' uses a release tag.\n */\n track?: 'release' | 'branch';\n\n /**\n * Pin the upstream ref for this run (analyze/upgrade), overriding `track`: a commit sha, a release\n * tag or an upstream branch name. Must be on the upstream branch or in an upstream release.\n */\n ref?: string;\n\n /**\n * Merge with the fork's CLI config as it stands (upgrade service). Without it, a new merge stops\n * before anything changes when upstream changed its own `overrides` or `packageJsonKeys` in ways\n * the fork config does not follow.\n */\n keepConfig?: boolean;\n\n /**\n * Lift every pinned entry for this run (analyze, sync): managed files stay pinned, and the merge\n * uses the full upstream history, so upstream changes that earlier syncs dropped come back too.\n */\n unpinned?: boolean;\n\n /** Count what the current branch changed since it left this ref, by kind of file (stats service) */\n since?: string;\n\n /** Print the `since` table as markdown for a pull request description (stats service) */\n md?: boolean;\n\n /** List every file instead of collapsing large groups (analyze), or every upstream alignment (align) */\n all?: boolean;\n\n /** Print one alignment's README (align service) */\n show?: string;\n\n /** Write one alignment's folder where its codemod can run (align service) */\n extract?: string;\n\n /** Run one alignment's codemod, keeping files identical to upstream as they are (align service) */\n run?: string;\n\n /** Arguments after `--`, passed to the codemod as given (align service, with `run`) */\n runArgs?: string[];\n\n /** Alignment ids to record as handled (align service) */\n mark?: string[];\n}\n\n/** File status after analysis */\nexport type FileStatus =\n | 'identical' // No changes needed\n | 'ahead' // Fork is ahead, protected (pinned/ignored)\n | 'local' // Local file (never existed upstream)\n | 'drifted' // Fork is ahead, NOT protected (at risk)\n | 'behind' // Upstream has changes to sync\n | 'diverged' // Both changed, will merge\n | 'pinned' // Fork wins on conflict\n | 'ignored' // Excluded from sync entirely\n | 'deleted' // Fork deleted, will stay deleted\n | 'renamed'; // Upstream renamed file\n\n/** Statuses the sync applies changes for: a staged sync holds at least one file with one of these. */\nexport const SYNC_APPLIED_STATUSES: readonly FileStatus[] = ['behind', 'diverged', 'renamed', 'ignored', 'pinned'];\n\n/** Commit metadata for a single log entry */\nexport interface CommitRangeEntry {\n hash: string;\n message: string;\n date: string;\n /** Unix epoch seconds of the commit */\n timestamp?: number;\n}\n\n/** Analyzed file with status and metadata */\nexport interface AnalyzedFile {\n path: string;\n status: FileStatus;\n /** True if file is in ignored list */\n isIgnored: boolean;\n /** True if file is in pinned list */\n isPinned: boolean;\n /** True if file exists in fork */\n existsInFork: boolean;\n /** True if file exists in upstream */\n existsInUpstream: boolean;\n /** Relative date when file was last changed (since merge-base) */\n changedAt?: string;\n /** Unix epoch seconds of the last change, for sorting */\n changedTs?: number;\n /** Short commit hash of the last change */\n changedCommit?: string;\n /** For diverged files: relative date of upstream change */\n upstreamChangedAt?: string;\n /** For diverged files: Unix epoch seconds of the upstream change, for sorting */\n upstreamChangedTs?: number;\n /** For diverged files: short commit hash of the upstream change */\n upstreamCommit?: string;\n /** For renamed files: the original path before rename */\n renamedFrom?: string;\n /**\n * Protected (pinned/ignored) files only: both sides changed since the merge-base. The fork\n * side wins whole-file, so upstream's changes to this path are dropped by the sync.\n */\n upstreamChanged?: boolean;\n /** For `upstreamChanged` and `upstreamOnly` files: lines upstream changed since the merge-base (undefined for binary) */\n upstreamChangedLines?: number;\n /**\n * Ignored files only: upstream changed, added or deleted the file since the merge-base and the\n * fork did not touch it. Ignored paths never sync, so without this flag the change stays unseen\n * (new config keys, version bumps). Managed files and generated output (`isGeneratedFile`) are\n * never flagged.\n */\n upstreamOnly?: boolean;\n /**\n * Pinned `ahead` files only: lines present upstream but absent from the fork (undefined for\n * binary). Independent of the sync point, so it also shows upstream content dropped by an\n * earlier sync, or removed on purpose; only a diff can tell.\n */\n upstreamLinesAbsent?: number;\n}\n\n/** Summary counts by status */\nexport interface AnalysisSummary {\n /** Changed package/config files that are handled separately from normal sync categories */\n managed: number;\n identical: number;\n ahead: number;\n local: number;\n drifted: number;\n behind: number;\n diverged: number;\n pinned: number;\n ignored: number;\n deleted: number;\n renamed: number;\n}\n\n/** Merge result from merge-engine */\nexport interface MergeResult {\n success: boolean;\n /** Upstream branch name for file links */\n upstreamBranch?: string;\n /** Resolved concrete upstream ref that was merged (branch ref or release tag ref) */\n upstreamRef?: string;\n /** Upstream release tag synced (when tracking releases) */\n upstreamTag?: string;\n files: AnalyzedFile[];\n summary: AnalysisSummary;\n conflicts: string[];\n /** Upstream GitHub URL base for commit links */\n upstreamGitHubUrl?: string;\n /** Upstream commit info */\n upstreamCommit?: CommitRangeEntry;\n /** Upstream commits since the last sync point */\n upstreamCommitCount?: number;\n /** Files that were auto-merged by git (diverged without remaining conflicts) */\n autoMergedFiles?: string[];\n /**\n * Protected (pinned/ignored) files resolved to the fork side although upstream also changed\n * them: upstream's hunks were dropped, not merged. Review each against upstream.\n */\n protectedConflicts?: string[];\n /** `<merge-base>..<upstream>` range of short SHAs: what upstream changed since the last sync, for the review page. */\n upstreamDiffRange?: string;\n /** How upstream's own CLI config changed since the merge-base, where the fork config does not follow. */\n upstreamOverrides?: UpstreamOverridesReport;\n /** Files flagged `upstreamOnly`, grouped by the `ignored` entry they fall under (config order). */\n ignoredUpstreamChanges?: IgnoredUpstreamGroup[];\n /**\n * Upstream alignments: how many the incoming ref holds, which ones upstream added since the\n * sync point, and the ones open in the fork after the run. A sync opens the arrivals; a dry run\n * leaves them out of `open`.\n */\n alignments?: { total: number; arrived: string[]; open: string[] };\n}\n\n/** Entries of one CLI config list (`pinned`, `ignored` or `packageJsonKeys`) that the fork config does not mirror. */\nexport interface OverrideListChanges {\n /** Entries upstream added since the merge-base that the fork list lacks. */\n added: string[];\n /** Entries upstream removed since the merge-base that the fork list still carries. */\n removed: string[];\n}\n\n/**\n * Upstream CLI config changes the fork should review. The CLI config is managed (never merged),\n * so new upstream `pinned`/`ignored` entries and `packageJsonKeys` only reach the fork by\n * hand. `unreadable` means upstream's config was missing or unparsable at one side (`base` =\n * merge-base, `incoming` = upstream ref), so nothing was compared.\n */\nexport type UpstreamOverridesReport =\n | {\n kind: 'changes';\n pinned: OverrideListChanges;\n ignored: OverrideListChanges;\n packageJsonKeys: OverrideListChanges;\n }\n | { kind: 'unreadable'; side: 'base' | 'incoming' };\n\n/** Upstream-only changes under one `ignored` entry. */\nexport interface IgnoredUpstreamGroup {\n /** The `ignored` entry (configured or derived from an app-owned module) the files fall under. */\n entry: string;\n /** Changed paths under the entry, sorted. */\n paths: string[];\n /** Paths the fork lacks (new upstream files). */\n added: number;\n /** Paths upstream deleted (the fork keeps them). */\n deleted: number;\n}\n"],"mappings":";AAKA,SAAS,SAAS;AAIlB,IAAM,oBAAoB;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAqHO,SAAS,aAAa,QAAwC;AACnE,SAAO;AACT;AAaO,IAAM,oBAAoB,EAC9B,OAAO;AAAA,EACN,UAAU,EACP,OAAO;AAAA,IACN,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IAC7B,gBAAgB,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,IAC3C,eAAe,EAAE,KAAK,CAAC,WAAW,QAAQ,CAAC,EAAE,SAAS;AAAA,IACtD,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,IACxC,iBAAiB,EAAE,MAAM,EAAE,KAAK,iBAAiB,CAAC,EAAE,SAAS;AAAA,IAC7D,kBAAkB,EAAE,QAAQ,EAAE,SAAS;AAAA,IACvC,iBAAiB,EAAE,MAAM,EAAE,KAAK,iBAAiB,CAAC,EAAE,SAAS;AAAA,IAC7D,kBAAkB,EAAE,QAAQ,EAAE,SAAS;AAAA,IACvC,cAAc,EAAE,KAAK,CAAC,UAAU,MAAM,CAAC,EAAE,SAAS;AAAA,EACpD,CAAC,EACA,OAAO;AAAA,EACV,WAAW,EACR,OAAO;AAAA,IACN,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS;AAAA,IACtC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS;AAAA,EACvC,CAAC,EACA,OAAO,EACP,SAAS;AAAA,EACZ,OAAO,EACJ;AAAA,IACC,EACG,OAAO;AAAA,MACN,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,MACtB,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,MAC3B,WAAW,EAAE,OAAO,EAAE,SAAS;AAAA,MAC/B,YAAY,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IAC9B,CAAC,EACA,OAAO;AAAA,EACZ,EACC,SAAS;AACd,CAAC,EACA,OAAO;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/config/types.ts"],"sourcesContent":["/**\n * Configuration types: the public cella/cella.config.ts surface, its runtime\n * schema, and the internal types the services share.\n */\n\nimport { z } from 'zod';\n\n// Public API for cella/cella.config.ts.\n\nconst PACKAGE_JSON_KEYS = [\n 'dependencies',\n 'devDependencies',\n 'peerDependencies',\n 'optionalDependencies',\n 'scripts',\n 'engines',\n 'packageManager',\n 'overrides',\n 'exports',\n 'pnpm',\n] as const;\n\n/** Valid package.json keys that can merge from upstream */\nexport type PackageJsonKey = (typeof PACKAGE_JSON_KEYS)[number];\n\n/**\n * Settings: all configurable options for the cella CLI.\n * Hover over each property for documentation.\n */\nexport interface SyncSettings {\n /** Upstream repository URL (e.g., 'git@github.com:cellajs/cella.git') */\n upstreamUrl: string;\n\n /**\n * Upstream branch, used for `upstreamTrack: 'branch'` and for GitHub file links.\n * Defaults to 'main'.\n */\n upstreamBranch?: string;\n\n /**\n * How to track upstream cella. Defaults to 'release'.\n * - 'release' (default): upgrade to a published cella release tag (`0.18.1`, with or without `v`). Stable and\n * reviewable: each bump maps to a changelog. Uses the latest release.\n * - 'branch': follow the bleeding-edge tip of `upstreamBranch`. For cella\n * maintainers and forks doing active development on top of unreleased changes.\n */\n upstreamTrack?: 'release' | 'branch';\n\n /**\n * Trunk branch that `pnpm cella upgrade` cuts the temporary branch from and that you open the\n * squash-merge PR into. Defaults to 'main'. Cutting fresh from trunk each cycle keeps every\n * PR limited to that cycle's upstream delta (no accumulation) and `main` linear.\n */\n releaseBase?: string;\n\n /** Which package.json keys merge from upstream (default: ['dependencies', 'devDependencies']). `type` always merges. */\n packageJsonKeys?: PackageJsonKey[];\n\n /** Merge the `packageJsonKeys` of every package.json after the upgrade merge (default: true). */\n mergePackageJson?: boolean;\n\n /** @deprecated Renamed to `packageJsonKeys`. */\n packageJsonSync?: PackageJsonKey[];\n\n /** @deprecated Renamed to `mergePackageJson`. */\n syncWithPackages?: boolean;\n\n /**\n * How to link files in CLI output.\n * - 'commit' (default): Link to the commit that changed the file on GitHub.\n * - 'file': Link to the file in the repo at the upstream branch on GitHub.\n */\n fileLinkMode?: 'commit' | 'file';\n}\n\n/**\n * Configuration for a downstream fork repository that cella interacts with.\n *\n * Cella is the upstream/source-of-truth. For each fork it can:\n * - pull contributions FROM the fork's `pullBranch` (contributions service)\n * - run the normal temporary-branch upgrade flow inside the fork (forks service)\n */\nexport interface ForkConfig {\n /** Display name for the fork (also used as the contrib/<name> branch slug) */\n name: string;\n /** Local path to the fork repository (relative to this config or absolute) */\n localPath: string;\n /**\n * Git remote URL of the fork (e.g. 'git@github.com:org/fork.git').\n * When set, the contributions service fetches the fork's `pullBranch` from this\n * remote (the authoritative committed ref), so the comparison does not depend on\n * the local checkout being up to date. The local clone is still used to read the\n * fork's owned-folder territory when available.\n */\n remoteUrl?: string;\n /** Fork branch that cella pulls contributions from (contributions service) */\n pullBranch: string;\n}\n\n/** User-configurable sync options for cella/cella.config.ts. */\nexport interface CellaCliConfig {\n /** Core sync settings */\n settings: SyncSettings;\n\n /**\n * File overrides for controlling sync behavior per folder/file.\n */\n overrides?: {\n /**\n * Paths the fork fully owns: never synced (existing or new).\n * Exact paths or directory prefixes, not globs: 'bench' matches 'bench/' and\n * everything under it, 'README.md' matches that exact file.\n * Local territory: upstream cannot add, modify, or delete anything under these.\n */\n ignored?: string[];\n\n /**\n * Paths pinned to the fork: the fork copy wins whole-file.\n * Exact paths or directory prefixes, not globs: 'bench' matches 'bench/' and\n * everything under it, 'README.md' matches that exact file.\n * Upstream changes to a pinned file are dropped, not merged: analyze lists them under\n * \"protected but behind upstream\" to adopt by hand.\n * If fork deleted a pinned file, deletion is respected.\n */\n pinned?: string[];\n };\n\n /**\n * Downstream fork repositories that cella interacts with (for upstream template repos).\n * When configured, the 'forks' and 'contributions' services appear in the CLI menu.\n */\n forks?: ForkConfig[];\n}\n\n/**\n * Helper function for defining cella/cella.config.ts with type checking.\n */\nexport function defineConfig(config: CellaCliConfig): CellaCliConfig {\n return config;\n}\n\n/**\n * Strict runtime schema mirroring {@link CellaCliConfig}.\n *\n * The TS interfaces above stay the source of truth (and keep their hover docs for\n * config authors). This schema is what's enforced at load time: `.strict()` rejects\n * unknown keys, so a typo like `ignoredFolders` for `ignored` fails closed; a silent\n * fallback would drop sync protection (tsx does not typecheck the config).\n *\n * The `satisfies z.ZodType<CellaCliConfig>` assertion keeps schema and interface in\n * lockstep: if either drifts (a renamed/removed/mis-typed field), TypeScript errors here.\n */\nexport const cellaConfigSchema = z\n .object({\n settings: z\n .object({\n upstreamUrl: z.string().min(1),\n upstreamBranch: z.string().min(1).optional(),\n upstreamTrack: z.enum(['release', 'branch']).optional(),\n releaseBase: z.string().min(1).optional(),\n packageJsonKeys: z.array(z.enum(PACKAGE_JSON_KEYS)).optional(),\n mergePackageJson: z.boolean().optional(),\n packageJsonSync: z.array(z.enum(PACKAGE_JSON_KEYS)).optional(),\n syncWithPackages: z.boolean().optional(),\n fileLinkMode: z.enum(['commit', 'file']).optional(),\n })\n .strict(),\n overrides: z\n .object({\n ignored: z.array(z.string()).optional(),\n pinned: z.array(z.string()).optional(),\n })\n .strict()\n .optional(),\n forks: z\n .array(\n z\n .object({\n name: z.string().min(1),\n localPath: z.string().min(1),\n remoteUrl: z.string().optional(),\n pullBranch: z.string().min(1),\n })\n .strict(),\n )\n .optional(),\n })\n .strict() satisfies z.ZodType<CellaCliConfig>;\n\n// Internal types for the services.\n\n/** Sync services available in the CLI */\nexport type SyncService = 'analyze' | 'upgrade' | 'align' | 'audit' | 'forks' | 'contributions' | 'stats';\n\n/** Where a command shows in the menu and in `--help`; a gate shows in `--help` only, since scripts and CI run it. */\nexport type CommandGroup = 'develop' | 'security' | 'upstream' | 'gate';\n\nexport type CommandOption = [flags: string, description: string];\n\n/**\n * A command that runs without the upgrade config: no `cella.config.ts`, no override validation. It declares its own\n * options, parses them into its own shape and returns its exit code.\n */\nexport interface ToolCommand<Options = unknown> {\n name: string;\n group: Exclude<CommandGroup, 'upstream'>;\n description: string;\n /** The jobs its first operand names, the default first; the menu asks for one. Empty: the command has one job. */\n jobs: [name: string, description: string][];\n /**\n * The words it takes after the job, in commander's notation such as `[scenario]` or `[paths...]`; none when unset.\n * A command takes these or a passthrough, never both.\n */\n operands?: CommandOption;\n options?: CommandOption[];\n /** What the words after `--` are for, in `--help`. */\n passthrough?: CommandOption;\n parse(input: { job?: string; operands: string[]; opts: Record<string, unknown>; passthrough: string[] }): Options;\n run(forkPath: string, options: Options): Promise<number>;\n}\n\n/** Analyze output scope for interactive and machine-readable flows */\nexport type AnalyzeScope = 'all' | 'risk' | 'protected';\n\n/** Runtime configuration with all resolved values */\nexport interface RuntimeConfig extends CellaCliConfig {\n /** Resolved fork repository path */\n forkPath: string;\n\n /** Full upstream remote ref (e.g., 'cella-upstream/development') */\n upstreamRef: string;\n\n /** Selected sync service */\n service: SyncService;\n\n /** Non-interactive list output (for LLM/agent usage) */\n list: boolean;\n\n /** Machine-readable JSON output (for tooling/agent usage) */\n json: boolean;\n\n /** Scope of files returned in analyze list/json output */\n scope?: AnalyzeScope;\n\n /** Show verbose output */\n verbose: boolean;\n\n /** Pre-selected fork name (skips fork selection prompt) */\n fork?: string;\n\n /**\n * Override upstream tracking for this run (analyze/upgrade), ignoring `settings.upstreamTrack`.\n * 'branch' follows the upstream branch tip (bleeding edge); 'release' uses a release tag.\n */\n track?: 'release' | 'branch';\n\n /**\n * Pin the upstream ref for this run (analyze/upgrade), overriding `track`: a commit sha, a release\n * tag or an upstream branch name. Must be on the upstream branch or in an upstream release.\n */\n ref?: string;\n\n /**\n * Merge with the fork's CLI config as it stands (upgrade service). Without it, a new merge stops\n * before anything changes when upstream changed its own `overrides` or `packageJsonKeys` in ways\n * the fork config does not follow.\n */\n keepConfig?: boolean;\n\n /**\n * Lift every pinned entry for this run (analyze, sync): managed files stay pinned, and the merge\n * uses the full upstream history, so upstream changes that earlier syncs dropped come back too.\n */\n unpinned?: boolean;\n\n /** Count what the current branch changed since it left this ref, by kind of file (stats service) */\n since?: string;\n\n /** Print the `since` table as markdown for a pull request description (stats service) */\n md?: boolean;\n\n /** List every file instead of collapsing large groups (analyze), or every upstream alignment (align) */\n all?: boolean;\n\n /** Print one alignment's README (align service) */\n show?: string;\n\n /** Write one alignment's folder where its codemod can run (align service) */\n extract?: string;\n\n /** Run one alignment's codemod, keeping files identical to upstream as they are (align service) */\n run?: string;\n\n /** Arguments after `--`, passed to the codemod as given (align service, with `run`) */\n runArgs?: string[];\n\n /** Alignment ids to record as handled (align service) */\n mark?: string[];\n}\n\n/** File status after analysis */\nexport type FileStatus =\n | 'identical' // No changes needed\n | 'ahead' // Fork is ahead, protected (pinned/ignored)\n | 'local' // Local file (never existed upstream)\n | 'drifted' // Fork is ahead, NOT protected (at risk)\n | 'behind' // Upstream has changes to sync\n | 'diverged' // Both changed, will merge\n | 'pinned' // Fork wins on conflict\n | 'ignored' // Excluded from sync entirely\n | 'deleted' // Fork deleted, will stay deleted\n | 'renamed'; // Upstream renamed file\n\n/** Statuses the sync applies changes for: a staged sync holds at least one file with one of these. */\nexport const SYNC_APPLIED_STATUSES: readonly FileStatus[] = ['behind', 'diverged', 'renamed', 'ignored', 'pinned'];\n\n/** Commit metadata for a single log entry */\nexport interface CommitRangeEntry {\n hash: string;\n message: string;\n date: string;\n /** Unix epoch seconds of the commit */\n timestamp?: number;\n}\n\n/** Analyzed file with status and metadata */\nexport interface AnalyzedFile {\n path: string;\n status: FileStatus;\n /** True if file is in ignored list */\n isIgnored: boolean;\n /** True if file is in pinned list */\n isPinned: boolean;\n /** True if file exists in fork */\n existsInFork: boolean;\n /** True if file exists in upstream */\n existsInUpstream: boolean;\n /** Relative date when file was last changed (since merge-base) */\n changedAt?: string;\n /** Unix epoch seconds of the last change, for sorting */\n changedTs?: number;\n /** Short commit hash of the last change */\n changedCommit?: string;\n /** For diverged files: relative date of upstream change */\n upstreamChangedAt?: string;\n /** For diverged files: Unix epoch seconds of the upstream change, for sorting */\n upstreamChangedTs?: number;\n /** For diverged files: short commit hash of the upstream change */\n upstreamCommit?: string;\n /** For renamed files: the original path before rename */\n renamedFrom?: string;\n /**\n * Protected (pinned/ignored) files only: both sides changed since the merge-base. The fork\n * side wins whole-file, so upstream's changes to this path are dropped by the sync.\n */\n upstreamChanged?: boolean;\n /** For `upstreamChanged` and `upstreamOnly` files: lines upstream changed since the merge-base (undefined for binary) */\n upstreamChangedLines?: number;\n /**\n * Ignored files only: upstream changed, added or deleted the file since the merge-base and the\n * fork did not touch it. Ignored paths never sync, so without this flag the change stays unseen\n * (new config keys, version bumps). Managed files and generated output (`isGeneratedFile`) are\n * never flagged.\n */\n upstreamOnly?: boolean;\n /**\n * Pinned `ahead` files only: lines present upstream but absent from the fork (undefined for\n * binary). Independent of the sync point, so it also shows upstream content dropped by an\n * earlier sync, or removed on purpose; only a diff can tell.\n */\n upstreamLinesAbsent?: number;\n}\n\n/** Summary counts by status */\nexport interface AnalysisSummary {\n /** Changed package/config files that are handled separately from normal sync categories */\n managed: number;\n identical: number;\n ahead: number;\n local: number;\n drifted: number;\n behind: number;\n diverged: number;\n pinned: number;\n ignored: number;\n deleted: number;\n renamed: number;\n}\n\n/** Merge result from merge-engine */\nexport interface MergeResult {\n success: boolean;\n /** Upstream branch name for file links */\n upstreamBranch?: string;\n /** Resolved concrete upstream ref that was merged (branch ref or release tag ref) */\n upstreamRef?: string;\n /** Upstream release tag synced (when tracking releases) */\n upstreamTag?: string;\n files: AnalyzedFile[];\n summary: AnalysisSummary;\n conflicts: string[];\n /** Upstream GitHub URL base for commit links */\n upstreamGitHubUrl?: string;\n /** Upstream commit info */\n upstreamCommit?: CommitRangeEntry;\n /** Upstream commits since the last sync point */\n upstreamCommitCount?: number;\n /** Files that were auto-merged by git (diverged without remaining conflicts) */\n autoMergedFiles?: string[];\n /**\n * Protected (pinned/ignored) files resolved to the fork side although upstream also changed\n * them: upstream's hunks were dropped, not merged. Review each against upstream.\n */\n protectedConflicts?: string[];\n /** `<merge-base>..<upstream>` range of short SHAs: what upstream changed since the last sync, for the review page. */\n upstreamDiffRange?: string;\n /** How upstream's own CLI config changed since the merge-base, where the fork config does not follow. */\n upstreamOverrides?: UpstreamOverridesReport;\n /** Files flagged `upstreamOnly`, grouped by the `ignored` entry they fall under (config order). */\n ignoredUpstreamChanges?: IgnoredUpstreamGroup[];\n /**\n * Upstream alignments: how many the incoming ref holds, which ones upstream added since the\n * sync point, and the ones open in the fork after the run. A sync opens the arrivals; a dry run\n * leaves them out of `open`.\n */\n alignments?: { total: number; arrived: string[]; open: string[] };\n}\n\n/** Entries of one CLI config list (`pinned`, `ignored` or `packageJsonKeys`) that the fork config does not mirror. */\nexport interface OverrideListChanges {\n /** Entries upstream added since the merge-base that the fork list lacks. */\n added: string[];\n /** Entries upstream removed since the merge-base that the fork list still carries. */\n removed: string[];\n}\n\n/**\n * Upstream CLI config changes the fork should review. The CLI config is managed (never merged),\n * so new upstream `pinned`/`ignored` entries and `packageJsonKeys` only reach the fork by\n * hand. `unreadable` means upstream's config was missing or unparsable at one side (`base` =\n * merge-base, `incoming` = upstream ref), so nothing was compared.\n */\nexport type UpstreamOverridesReport =\n | {\n kind: 'changes';\n pinned: OverrideListChanges;\n ignored: OverrideListChanges;\n packageJsonKeys: OverrideListChanges;\n }\n | { kind: 'unreadable'; side: 'base' | 'incoming' };\n\n/** Upstream-only changes under one `ignored` entry. */\nexport interface IgnoredUpstreamGroup {\n /** The `ignored` entry (configured or derived from an app-owned module) the files fall under. */\n entry: string;\n /** Changed paths under the entry, sorted. */\n paths: string[];\n /** Paths the fork lacks (new upstream files). */\n added: number;\n /** Paths upstream deleted (the fork keeps them). */\n deleted: number;\n}\n"],"mappings":";AAKA,SAAS,SAAS;AAIlB,IAAM,oBAAoB;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAqHO,SAAS,aAAa,QAAwC;AACnE,SAAO;AACT;AAaO,IAAM,oBAAoB,EAC9B,OAAO;AAAA,EACN,UAAU,EACP,OAAO;AAAA,IACN,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IAC7B,gBAAgB,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,IAC3C,eAAe,EAAE,KAAK,CAAC,WAAW,QAAQ,CAAC,EAAE,SAAS;AAAA,IACtD,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,IACxC,iBAAiB,EAAE,MAAM,EAAE,KAAK,iBAAiB,CAAC,EAAE,SAAS;AAAA,IAC7D,kBAAkB,EAAE,QAAQ,EAAE,SAAS;AAAA,IACvC,iBAAiB,EAAE,MAAM,EAAE,KAAK,iBAAiB,CAAC,EAAE,SAAS;AAAA,IAC7D,kBAAkB,EAAE,QAAQ,EAAE,SAAS;AAAA,IACvC,cAAc,EAAE,KAAK,CAAC,UAAU,MAAM,CAAC,EAAE,SAAS;AAAA,EACpD,CAAC,EACA,OAAO;AAAA,EACV,WAAW,EACR,OAAO;AAAA,IACN,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS;AAAA,IACtC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS;AAAA,EACvC,CAAC,EACA,OAAO,EACP,SAAS;AAAA,EACZ,OAAO,EACJ;AAAA,IACC,EACG,OAAO;AAAA,MACN,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,MACtB,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,MAC3B,WAAW,EAAE,OAAO,EAAE,SAAS;AAAA,MAC/B,YAAY,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IAC9B,CAAC,EACA,OAAO;AAAA,EACZ,EACC,SAAS;AACd,CAAC,EACA,OAAO;","names":[]}
|