/**
 * release-notes/publish.mjs — `gh release create/edit` shell.
 *
 * Reads `<output_dir>/<tag>.md` and shells `gh release create <tag>
 * --notes-file <path>` (or `gh release edit` if a release already exists
 * for that tag). When `gh` is unavailable on the host, returns a graceful
 * `{status: 'skipped', reason: 'gh-not-found'}` envelope rather than
 * crashing — AC4.
 *
 * Tests inject a custom shell via the GH_BIN env var (mocked to a no-op
 * shell script) so the publish branch of TD-5 can run without making a
 * real network call.
 */
import { execFileSync, spawnSync } from 'node:child_process';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { resolveOutputDir, validateTag } from './output-dir.mjs';
/**
 * @typedef {Object} PublishInput
 * @property {string} projectDir
 * @property {string} tag
 * @property {string | null} outputDir
 * @property {boolean} dryRun
 */
/**
 * @typedef {{
 *   status: 'ok',
 *   mode: 'created' | 'edited' | 'dry-run',
 *   notesPath: string,
 * } | {
 *   status: 'skipped',
 *   reason: 'gh-not-found' | 'notes-not-found' | 'gh-failed',
 *   notesPath?: string,
 *   exit_code?: number | null,
 *   stderr?: string | null,
 * }} PublishResult
 */
/**
 * Resolve which `gh` binary to use. Defaults to `gh` on PATH; tests
 * override via the GH_BIN env var (only honored in NODE_ENV=test).
 *
 * @returns {string}
 */
function ghBin() {
    if (process.env.NODE_ENV === 'test') {
        const override = process.env.GH_BIN;
        if (override && override.length > 0)
            return override;
    }
    return 'gh';
}
/**
 * Detect whether `gh` is available on the host. Uses `which` semantics:
 * `gh --version` exit-0 → available. Any error path → unavailable.
 *
 * @returns {boolean}
 */
function ghAvailable() {
    try {
        const result = spawnSync(ghBin(), ['--version'], { stdio: ['ignore', 'ignore', 'ignore'] });
        return result.status === 0;
    }
    catch {
        return false;
    }
}
/**
 * Check whether a release already exists for the given tag.
 *
 * @param {string} projectDir
 * @param {string} tag
 * @returns {boolean}
 */
function releaseExists(projectDir, tag) {
    try {
        const result = spawnSync(ghBin(), ['release', 'view', tag, '--json', 'tagName'], {
            cwd: projectDir,
            stdio: ['ignore', 'pipe', 'ignore'],
        });
        return result.status === 0;
    }
    catch {
        return false;
    }
}
/**
 * @param {PublishInput} input
 * @returns {PublishResult}
 */
export function publishTag(input) {
    validateTag(input.tag);
    const outputDir = resolveOutputDir(input.projectDir, input.outputDir);
    const notesPath = join(outputDir, `${input.tag}.md`);
    if (!existsSync(notesPath)) {
        return { status: 'skipped', reason: 'notes-not-found', notesPath };
    }
    if (input.dryRun) {
        return { status: 'ok', mode: 'dry-run', notesPath };
    }
    if (!ghAvailable()) {
        return { status: 'skipped', reason: 'gh-not-found', notesPath };
    }
    const exists = releaseExists(input.projectDir, input.tag);
    const verb = exists ? 'edit' : 'create';
    const args = ['release', verb, input.tag, '--notes-file', notesPath];
    // `gh release edit` only takes --notes-file; `gh release create` takes
    // --notes-file too and a positional tag — same argv order works.
    try {
        execFileSync(ghBin(), args, {
            cwd: input.projectDir,
            stdio: ['ignore', 'pipe', 'pipe'],
        });
    }
    catch (err) {
        return {
            status: 'skipped',
            reason: 'gh-failed',
            notesPath,
            exit_code: err.status ?? null,
            stderr: err.stderr?.toString() ?? null,
        };
    }
    return { status: 'ok', mode: exists ? 'edited' : 'created', notesPath };
}
//# sourceMappingURL=publish.mjs.map