"use strict";
/**
 * Copyright: (c) Myia SAS 2026.
 * This file and its contents are licensed under the AGPLv3 License.
 * Please see the LICENSE file at the root of this repository
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.HealTracerReporter = exports.HEAL_PENDING_SUBDIR = void 0;
exports.healPendingRegistryPath = healPendingRegistryPath;
// Optional Playwright `Reporter` that runs in the MAIN process and
// rescues per-test NDJSON files when the worker dies before the
// fixture's teardown can finalize them.
//
// Crash paths the fixture cannot handle on its own (worker OOM,
// SIGKILL, segfault, `process.exit()`): the fixture's `finally`
// block never runs, so `projector.finalize(...)` never writes the
// `test-result` terminator. The NDJSON is left open-ended, and
// consumers cannot tell whether the test crashed or the file is
// simply still being written.
//
// The reporter subscribes to `onTestEnd` — which fires even when
// the worker died, because Playwright fabricates the result from
// the main-process side. It then:
//
//   1. Resolves the per-test NDJSON path + outputDir from a
//      filesystem REGISTRY the fixture writes at test setup:
//      `<project.outputDir>/.heal-pending/<testId>-<attempt>.json`.
//      The registry is needed because Playwright's IPC does NOT
//      flush fixture-pushed annotations/attachments on abrupt
//      worker exit (OOM/SIGKILL/`process.exit()`) — exactly the
//      cases we most need to rescue. A file on disk survives any
//      abrupt exit. The fixture deletes the registry entry at
//      teardown, so only crashed tests leave an orphan behind.
//      This reporter is then the only thing that reads the orphan.
//   2. Early-returns if the registry entry is missing (tracer not
//      wired for this test, or fixture already deleted it on clean
//      teardown) or if the NDJSON already ends with `test-result`
//      (defensive — shouldn't happen given the deletion step).
//   3. Classifies the crash via `CrashErrorClassifier` using the
//      worker's stderr buffer and `TestResult.errors`, then appends
//      a synthetic `test-result` line with the classified error.
//   4. If an `onRescue` hook is configured, invokes it (fire-and-
//      forget) so extension code can observe the rescued record.
//
// Registration (user-facing, in `playwright.config.ts`):
//
//   reporter: [['@heal-dev/heal-playwright-tracer/reporter']]
//
// Registration is mandatory whenever the Babel plugin is wired —
// the fixture asserts on the `HEAL_TRACER_REPORTER` env var (set
// here in `onBegin`) and fails fast if it's missing.
const child_process_1 = require("child_process");
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const persistence_1 = require("../../domain/persistence");
const heal_traces_layout_1 = require("../heal-traces-layout");
const crash_error_classifier_1 = require("./crash-error-classifier");
const failing_statement_finder_1 = require("./failing-statement-finder");
const ndjson_tail_inspector_1 = require("./ndjson-tail-inspector");
const logger_1 = require("../../util/logger");
/**
 * Subdirectory under each project's `outputDir` where the fixture
 * writes a registry entry per (testId, attempt). Shared string
 * constant between fixture (writer) and reporter (reader).
 */
exports.HEAL_PENDING_SUBDIR = '.heal-pending';
/** Path to the registry file for a given (project, test, attempt). */
function healPendingRegistryPath(projectOutputDir, testId, attempt) {
    return path.join(projectOutputDir, exports.HEAL_PENDING_SUBDIR, `${testId}-${attempt}.json`);
}
class HealTracerReporter {
    constructor(deps = {}) {
        // In-flight hook promises — awaited by `onEnd` so the trailing
        // test's hook (which typically POSTs artefacts to a remote
        // destination) has a chance to complete before the process exits.
        this.pendingAttachmentsHooks = [];
        this.stderrByWorker = new Map();
        this.projectOutputDirs = [];
        // Execution history (P2) — reporter is the only writer of
        // executions.ndjson + execution.json. Resolved at onBegin so the
        // executionId propagates to workers via HEAL_EXECUTION_ID.
        this.executionId = '';
        this.executionSource = 'generated';
        this.startedAt = 0;
        this.rootDir = '';
        // Keyed by playwrightTestId so retries fold into the same test entry.
        // Insertion order = first-seen test order, which the manifest preserves.
        this.testEntriesById = new Map();
        this.classifier = deps.classifier ?? new crash_error_classifier_1.CrashErrorClassifier();
        this.inspector = deps.inspector ?? new ndjson_tail_inspector_1.NdjsonTailInspector();
        this.failingStatementFinder = deps.failingStatementFinder ?? new failing_statement_finder_1.FailingStatementFinder();
        this.appendFile = deps.appendFile ?? ((p, d) => fs.appendFileSync(p, d, { encoding: 'utf8' }));
        this.onRescue = deps.onRescue ?? null;
        this.onAttachmentsWritten = deps.onAttachmentsWritten ?? null;
    }
    printsToStdio() {
        return false;
    }
    onBegin(config, _suite) {
        // Capture every project's outputDir so we know where to look
        // for registry entries regardless of which project a given
        // TestCase belongs to. Deduped — sharded configs sometimes
        // repeat the same outputDir across projects.
        const seen = new Set();
        for (const project of config.projects) {
            if (project.outputDir && !seen.has(project.outputDir)) {
                seen.add(project.outputDir);
                this.projectOutputDirs.push(project.outputDir);
            }
        }
        // Resolve executionId once in main BEFORE workers spawn, then
        // propagate via env so worker fixtures see the same value.
        // Workers' resolver reports `source='env'` after this — that's
        // expected; the truthful source we record on the run is OURS,
        // captured here before we mutate the env var.
        this.executionSource = (0, heal_traces_layout_1.getExecutionIdSource)();
        this.executionId = (0, heal_traces_layout_1.resolveExecutionId)();
        if (this.executionSource === 'generated') {
            process.env.HEAL_EXECUTION_ID = this.executionId;
        }
        // Worker fixtures assert on this env var to detect when a user has
        // wired the Babel plugin but forgotten to register the reporter.
        // Setting it here (before workers spawn) is the handshake; the
        // fixture's check is the enforcement.
        process.env.HEAL_TRACER_REPORTER = '1';
        this.startedAt = Date.now();
        this.rootDir = process.cwd();
        this.playwrightVersion = readPlaywrightVersion();
        this.gitInfo = readGitInfo();
    }
    onTestBegin(_test, result) {
        this.stderrByWorker.delete(result.workerIndex);
    }
    onStdErr(chunk, _test, result) {
        if (!result)
            return;
        const text = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
        const buf = this.stderrByWorker.get(result.workerIndex) ?? [];
        buf.push(text);
        this.stderrByWorker.set(result.workerIndex, buf);
    }
    onTestEnd(test, result) {
        const traceCtx = this.resolveTraceContext(test, result);
        // Tracer not wired for this test (no registry entry) or the
        // worker never started a trace file — record what Playwright
        // already knows and stop. No failing-statement source on disk.
        if (!traceCtx || !fs.existsSync(traceCtx.ndjsonPath)) {
            this.captureAttempt(test, result, null, null, null);
            if (traceCtx)
                this.cleanupRegistry(test, result);
            return;
        }
        let rescuedRecord = null;
        // Rescue path: file is missing the `test-result` terminator.
        // Synthesize one from the worker's stderr buffer + Playwright's
        // post-mortem `result.errors`.
        if (!this.inspector.endsWithTestResult(traceCtx.ndjsonPath)) {
            const stderr = (this.stderrByWorker.get(result.workerIndex) ?? []).join('');
            const error = this.classifier.classify(result.errors, stderr);
            rescuedRecord = this.buildSyntheticTestResult(result, stderr, error);
            try {
                this.appendFile(traceCtx.ndjsonPath, JSON.stringify(rescuedRecord) + '\n');
            }
            catch (err) {
                logger_1.log.error(`failed to append synthetic test-result to ${traceCtx.ndjsonPath}`, err);
                this.captureAttempt(test, result, null, null, null);
                this.cleanupRegistry(test, result);
                return;
            }
        }
        // Always-on: copy each Playwright attachment from its outputDir
        // location into the persistent heal-traces tree, then append a
        // `test-attachments` record whose paths are relative to that
        // tree. Keeps the file shape canonical (test-result then
        // test-attachments) and makes the heal-traces dir the single
        // source of truth — Playwright's `outputDir` can be wiped, the
        // history survives.
        const attachmentsRecord = this.copyAndBuildAttachmentsRecord(result, traceCtx);
        let attachmentsAppended = false;
        try {
            this.appendFile(traceCtx.ndjsonPath, JSON.stringify(attachmentsRecord) + '\n');
            attachmentsAppended = true;
        }
        catch (err) {
            logger_1.log.error(`failed to append test-attachments to ${traceCtx.ndjsonPath}`, err);
        }
        const failureScreenshot = this.pickFailureScreenshot(attachmentsRecord.attachments);
        this.captureAttempt(test, result, traceCtx.ndjsonPath, rescuedRecord, failureScreenshot);
        this.cleanupRegistry(test, result);
        if (rescuedRecord) {
            this.invokeRescueHook(rescuedRecord, traceCtx, test, result);
        }
        // Fire the attachments hook only when the record actually hit
        // disk — a downstream that reads the NDJSON expecting consistency
        // with what was POSTed would otherwise see drift.
        if (attachmentsAppended) {
            this.invokeAttachmentsHook(attachmentsRecord, traceCtx, test, result);
        }
    }
    // Probes each known project outputDir for the per-test registry
    // entry. First match wins — the same (testId, attempt) never
    // spans projects. Returns null when no entry exists, which means
    // either the tracer wasn't wired for this test or the fixture
    // already cleaned up on a clean teardown.
    resolveTraceContext(test, result) {
        const attempt = result.retry + 1;
        for (const outputDir of this.projectOutputDirs) {
            const entryPath = healPendingRegistryPath(outputDir, test.id, attempt);
            let body;
            try {
                body = fs.readFileSync(entryPath, 'utf8');
            }
            catch {
                continue;
            }
            try {
                const parsed = JSON.parse(body);
                if (typeof parsed.ndjsonPath !== 'string' || parsed.ndjsonPath.length === 0)
                    continue;
                if (typeof parsed.rootDir !== 'string' || parsed.rootDir.length === 0)
                    continue;
                if (typeof parsed.playwrightOutputDir !== 'string' ||
                    parsed.playwrightOutputDir.length === 0) {
                    continue;
                }
                if (typeof parsed.executionId !== 'string' || parsed.executionId.length === 0) {
                    continue;
                }
                return {
                    ndjsonPath: parsed.ndjsonPath,
                    rootDir: parsed.rootDir,
                    playwrightOutputDir: parsed.playwrightOutputDir,
                    executionId: parsed.executionId,
                    videoPages: sanitizeVideoPages(parsed.videoPages),
                };
            }
            catch {
                continue;
            }
        }
        return null;
    }
    /**
     * Copy each Playwright attachment from its `outputDir` location
     * into the persistent heal-traces tree (under `traceCtx.rootDir`),
     * mapping by `name` / `contentType`:
     *
     *   - 'trace'                     → <rootDir>/trace.zip
     *   - contentType: 'video/...'    → <rootDir>/videos/<basename>
     *   - 'screenshot' + image/...    → <rootDir>/screenshots/<basename>
     *   - other (user attachments)    → <rootDir>/<rel-from-outputDir>
     *
     * Returns a `test-attachments` record whose `path` fields are the
     * destination-relative locations using forward slashes, so the
     * local-viewer-server can serve them via `/api/asset/...`.
     *
     * Attachments whose source path falls outside Playwright's
     * outputDir are dropped — those would not be safely servable.
     */
    copyAndBuildAttachmentsRecord(result, traceCtx) {
        const srcRoot = path.resolve(traceCtx.playwrightOutputDir);
        const dstRoot = path.resolve(traceCtx.rootDir);
        const attachments = [];
        // Positional join with the fixture's `videoPages` array: the Nth
        // video attachment in `result.attachments` pairs with the Nth
        // entry in `videoPages`. We can't join by path because Playwright
        // renames the video file between fixture teardown (recording-time
        // hash name) and reporter onTestEnd (final `video[-N].webm`).
        // Both sides walk pages/attachments in creation order, so index
        // is stable.
        const videoPages = traceCtx.videoPages ?? [];
        let videoIndex = 0;
        for (const att of result.attachments ?? []) {
            if (!att.path)
                continue;
            const srcResolved = path.resolve(att.path);
            const relFromSrc = path.relative(srcRoot, srcResolved);
            if (relFromSrc.length === 0 || relFromSrc.startsWith('..') || path.isAbsolute(relFromSrc)) {
                continue;
            }
            const dstRel = this.placeAttachment(att.name, att.contentType, relFromSrc);
            const dstAbs = path.resolve(dstRoot, dstRel);
            try {
                fs.mkdirSync(path.dirname(dstAbs), { recursive: true });
                fs.copyFileSync(srcResolved, dstAbs);
            }
            catch (err) {
                logger_1.log.error(`failed to copy attachment ${att.name} (${srcResolved} → ${dstAbs})`, err);
                continue;
            }
            // Forward slashes on all platforms — the local viewer maps
            // these into URLs and Windows paths break URL routing.
            const rel = dstRel.split(path.sep).join('/');
            const entry = {
                name: att.name,
                path: rel,
                contentType: att.contentType,
            };
            if (att.contentType.toLowerCase().startsWith('video/')) {
                const vp = videoPages[videoIndex];
                videoIndex += 1;
                if (vp) {
                    entry.pageName = vp.name;
                    entry.pageUrl = vp.url;
                }
            }
            attachments.push(entry);
        }
        return { kind: 'test-attachments', attachments };
    }
    placeAttachment(name, contentType, relFromSrc) {
        if (name === 'trace') {
            return 'trace.zip';
        }
        if (contentType.toLowerCase().startsWith('video/')) {
            return path.join('videos', path.basename(relFromSrc));
        }
        // Playwright's `screenshot: 'only-on-failure' | 'on'` (and any
        // user `testInfo.attach('screenshot', …)` image) lands in the
        // same `screenshots/` subdir as the per-statement highlight
        // PNGs, so all screenshots for a test live in one place.
        if (name === 'screenshot' && contentType.toLowerCase().startsWith('image/')) {
            return path.join('screenshots', path.basename(relFromSrc));
        }
        return relFromSrc;
    }
    cleanupRegistry(test, result) {
        const attempt = result.retry + 1;
        for (const outputDir of this.projectOutputDirs) {
            const entryPath = healPendingRegistryPath(outputDir, test.id, attempt);
            try {
                fs.unlinkSync(entryPath);
            }
            catch {
                /* entry may not exist; ignore */
            }
        }
    }
    buildSyntheticTestResult(result, stderr, error) {
        const record = {
            kind: 'test-result',
            status: result.status,
            duration: result.duration,
            error,
        };
        if (stderr.length > 0)
            record.stderr = [stderr];
        return record;
    }
    invokeRescueHook(record, traceCtx, test, result) {
        if (!this.onRescue)
            return;
        const rescueCtx = {
            ndjsonPath: traceCtx.ndjsonPath,
            rootDir: traceCtx.rootDir,
            playwrightOutputDir: traceCtx.playwrightOutputDir,
            executionId: traceCtx.executionId,
            testId: test.id,
            attempt: result.retry + 1,
            workerIndex: result.workerIndex,
        };
        // Fire-and-forget: Playwright's `onTestEnd` is typed as sync
        // `void`, and the synthetic record is already durably on disk.
        // A slow or failing hook must not block the reporter or
        // propagate errors to the Playwright run.
        void Promise.resolve()
            .then(() => this.onRescue(record, rescueCtx))
            .catch((err) => {
            logger_1.log.error('onRescue hook failed', err);
        });
    }
    invokeAttachmentsHook(record, traceCtx, test, result) {
        if (!this.onAttachmentsWritten)
            return;
        const ctx = {
            ndjsonPath: traceCtx.ndjsonPath,
            rootDir: traceCtx.rootDir,
            playwrightOutputDir: traceCtx.playwrightOutputDir,
            executionId: traceCtx.executionId,
            testId: test.id,
            attempt: result.retry + 1,
            workerIndex: result.workerIndex,
        };
        // Tracked: `onEnd` awaits these so a trailing test's hook (which
        // typically POSTs artefacts to a remote destination) has time to
        // complete before the process exits. Errors swallowed + logged
        // here so a slow or failing hook still doesn't block the
        // Playwright run.
        const promise = Promise.resolve()
            .then(() => this.onAttachmentsWritten(record, ctx))
            .catch((err) => {
            logger_1.log.error('onAttachmentsWritten hook failed', err);
        });
        this.pendingAttachmentsHooks.push(promise);
    }
    /**
     * Upsert the per-test entry for `test` and append an attempt slice
     * built from `result`. When `ndjsonPath` is non-null and the
     * attempt did not pass, scans the file for the deepest threw
     * statement and attaches it as `failingStatement` + `error`. A
     * `rescuedRecord` (synthesized by the rescue path) supplies
     * `error` when no statement-level threw is present.
     * `failureScreenshot` (the heal-traces-relative path to
     * Playwright's failure screenshot, already copied into the tree)
     * is attached independently whenever the attempt did not
     * pass/skip and Playwright produced one.
     */
    captureAttempt(test, result, ndjsonPath, rescuedRecord, failureScreenshot) {
        const entry = this.upsertTestEntry(test);
        const attempt = {
            attempt: result.retry + 1,
            status: this.mapTestStatus(result.status),
            startedAt: result.startTime instanceof Date ? result.startTime.getTime() : Date.now(),
            durationMs: result.duration,
        };
        if (attempt.status !== 'passed' && attempt.status !== 'skipped') {
            const found = ndjsonPath ? this.failingStatementFinder.find(ndjsonPath) : null;
            if (found) {
                attempt.failingStatement = found.statement;
                attempt.error = found.error;
            }
            else if (rescuedRecord?.error) {
                attempt.error = rescuedRecord.error;
            }
            if (failureScreenshot) {
                attempt.failureScreenshot = failureScreenshot;
            }
        }
        entry.attempts.push(attempt);
    }
    // Playwright's `screenshot: 'only-on-failure' | 'on'` produces an
    // attachment named exactly `screenshot` with an image contentType.
    // The strict name match excludes user `testInfo.attach()` images.
    // With several pages open Playwright emits one screenshot per
    // page; the first is the primary page the test drove, which is
    // the one worth surfacing on the attempt.
    pickFailureScreenshot(attachments) {
        const shot = attachments.find((a) => a.name === 'screenshot' && a.contentType.toLowerCase().startsWith('image/'));
        return shot?.path ?? null;
    }
    upsertTestEntry(test) {
        const existing = this.testEntriesById.get(test.id);
        if (existing)
            return existing;
        const titlePath = typeof test.titlePath === 'function'
            ? test.titlePath()
            : [];
        const file = test.location?.file ?? '';
        const project = test.parent?.project?.()
            ?.name ?? '';
        // Playwright's `TestCase.tags` already merges tags from enclosing
        // describe blocks; we persist them verbatim — no parsing or
        // de-duplication here (consumers like heal-cli own any
        // tag-prefix interpretation).
        const tags = test.tags ?? [];
        const entry = {
            playwrightTestId: test.id,
            title: test.title,
            titlePath,
            file,
            project,
            tags,
            attempts: [],
        };
        this.testEntriesById.set(test.id, entry);
        return entry;
    }
    mapTestStatus(s) {
        switch (s) {
            case 'passed':
            case 'failed':
            case 'timedOut':
            case 'skipped':
            case 'interrupted':
                return s;
            default:
                return 'unknown';
        }
    }
    async onEnd() {
        // Await in-flight `onAttachmentsWritten` hooks first so trailing
        // tests' artefact-shipping completes before the manifest write
        // and process exit. `allSettled` because errors are already
        // logged inside the hook wrapper — we just need to wait.
        if (this.pendingAttachmentsHooks.length > 0) {
            await Promise.allSettled(this.pendingAttachmentsHooks);
        }
        if (this.executionId.length === 0)
            return;
        const layout = new heal_traces_layout_1.HealTracesLayout(this.rootDir, this.executionId);
        const endedAt = Date.now();
        const tests = Array.from(this.testEntriesById.values());
        const totals = computeTotals(tests);
        const manifest = {
            executionId: this.executionId,
            source: this.executionSource,
            startedAt: this.startedAt,
            endedAt,
            durationMs: endedAt - this.startedAt,
            ...(this.gitInfo ? { git: this.gitInfo } : {}),
            ...(this.playwrightVersion ? { playwrightVersion: this.playwrightVersion } : {}),
            totals,
            tests,
        };
        const record = {
            kind: persistence_1.EXECUTION_RECORD_KIND,
            executionId: this.executionId,
            source: this.executionSource,
            startedAt: this.startedAt,
            endedAt,
            durationMs: endedAt - this.startedAt,
            ...(this.gitInfo ? { git: this.gitInfo } : {}),
            ...(this.playwrightVersion ? { playwrightVersion: this.playwrightVersion } : {}),
            totals,
        };
        try {
            fs.mkdirSync(layout.executionDir(), { recursive: true });
            fs.writeFileSync(layout.executionManifestPath(), JSON.stringify(manifest, null, 2), 'utf8');
        }
        catch (err) {
            logger_1.log.error('failed to write execution.json', err);
        }
        try {
            fs.mkdirSync(layout.healTracesRoot(), { recursive: true });
            this.appendFile(layout.executionsNdjsonPath(), JSON.stringify(record) + '\n');
        }
        catch (err) {
            logger_1.log.error('failed to append executions.ndjson', err);
        }
    }
}
exports.HealTracerReporter = HealTracerReporter;
// Per-attempt totals: each retry counts as a separate row, matching
// the pre-grouping semantics. A flaky test that fails-then-passes
// surfaces as 1 passed + 1 failed against `tests: 2`.
function computeTotals(entries) {
    const totals = {
        tests: 0,
        passed: 0,
        failed: 0,
        timedOut: 0,
        skipped: 0,
        interrupted: 0,
    };
    for (const e of entries) {
        for (const a of e.attempts) {
            totals.tests += 1;
            if (a.status === 'passed')
                totals.passed += 1;
            else if (a.status === 'failed')
                totals.failed += 1;
            else if (a.status === 'timedOut')
                totals.timedOut += 1;
            else if (a.status === 'skipped')
                totals.skipped += 1;
            else if (a.status === 'interrupted')
                totals.interrupted += 1;
            // 'unknown' counts toward `tests` but not any per-status bucket.
        }
    }
    return totals;
}
// Lenient parse of the registry's optional `videoPages`. Never
// throws and never rejects the surrounding entry — a malformed map
// just means video attachments go un-enriched, not that the trace
// context is unusable. Entries missing any field are dropped.
function sanitizeVideoPages(raw) {
    if (!Array.isArray(raw))
        return undefined;
    const out = [];
    for (const item of raw) {
        if (!item || typeof item !== 'object')
            continue;
        const { name, url } = item;
        if (typeof name !== 'string' || name.length === 0)
            continue;
        if (typeof url !== 'string')
            continue;
        out.push({ name, url });
    }
    return out.length > 0 ? out : undefined;
}
function readPlaywrightVersion() {
    try {
        const pkg = require('@playwright/test/package.json');
        return typeof pkg.version === 'string' ? pkg.version : undefined;
    }
    catch {
        return undefined;
    }
}
function readGitInfo() {
    const sha = tryGit('rev-parse HEAD');
    const branch = tryGit('rev-parse --abbrev-ref HEAD');
    const dirty = tryGit('status --porcelain');
    if (sha === undefined && branch === undefined && dirty === undefined) {
        return undefined;
    }
    const info = {};
    if (sha !== undefined)
        info.sha = sha;
    if (branch !== undefined && branch !== 'HEAD')
        info.branch = branch;
    if (dirty !== undefined)
        info.dirty = dirty.length > 0;
    return info;
}
function tryGit(args) {
    try {
        return (0, child_process_1.execSync)(`git ${args}`, {
            stdio: ['ignore', 'pipe', 'ignore'],
        })
            .toString()
            .trim();
    }
    catch {
        return undefined;
    }
}
