import { createInterface } from 'node:readline';
/**
 * Resolves whether non-interactive mode is requested.
 * Handles both explicit `noInteractive: true` and Commander.js style `interactive: false`.
 * Use this helper instead of manually checking options.noInteractive to avoid bugs.
 */
export function resolveNoInteractive(value) {
    if (typeof value === 'boolean')
        return value;
    return value?.noInteractive === true || value?.interactive === false;
}
export function isInteractive(value) {
    if (resolveNoInteractive(value))
        return false;
    if (process.env.OPEN_SPEC_INTERACTIVE === '0')
        return false;
    // Respect the standard CI environment variable (set by GitHub Actions, GitLab CI, Travis, etc.)
    if ('CI' in process.env)
        return false;
    return !!process.stdin.isTTY;
}
/**
 * True when a prompt failed because no answer could be read — an agent or a
 * script that ran the command with stdin closed, a CI job, or a shell whose
 * stdin is not a terminal. @inquirer rejects those with `User force closed
 * the prompt with 0 null`, which is accurate and useless: it names no flag
 * and no next step (#1479).
 *
 * Two things it deliberately is not:
 *
 * - It is not a substitute for `isInteractive()`. This classifies a prompt
 *   that has *already failed*, so piped answers are unaffected: an answer
 *   that arrives resolves the prompt and never reaches this check. Refusing
 *   to prompt up front would break `printf 'y\n' | openspec archive ...`,
 *   which works today.
 * - It is not a cancellation check. Ctrl-C raises the same error class, and
 *   it reaches a process whose stdin is a pipe just as easily as one at a
 *   terminal, so the SIGINT signal - not the terminal - is what proves
 *   somebody was there and chose to quit.
 *
 * Beyond that it defers to `isInteractive()`, so `CI`, `OPEN_SPEC_INTERACTIVE=0`
 * and `--no-interactive` count even when a runner allocated a pty. It also
 * counts a redirected stdout: `confirmPrompt` drops to the plain reader whenever
 * *either* stream is not a TTY, so a stdin-TTY-but-stdout-redirected run
 * (`openspec archive x > log.txt` from a terminal) that hits EOF must classify
 * the same way the prompt was selected — otherwise it would leak the raw
 * `ExitPromptError` instead of the `--yes` guidance.
 */
export function isNonInteractivePromptError(error, value) {
    if (!(error instanceof Error))
        return false;
    const failedPrompt = error.name === 'ExitPromptError' || error.message.includes('force closed the prompt');
    if (!failedPrompt)
        return false;
    if (error.message.includes('SIGINT'))
        return false;
    return !isInteractive(value) || !process.stdout.isTTY;
}
/**
 * Ask a yes/no question. A real terminal gets @inquirer's rich prompt;
 * everything else — a pipe, a file redirect, an agent that captures stdout —
 * reads one plain line instead.
 *
 * @inquirer renders `confirm` by writing ANSI cursor-movement escape sequences,
 * and it emits them even when stdout is not a TTY. Redirected to a file those
 * sequences are noise, and in some non-TTY hosts the render loop never settles
 * and repeats `ESC[NNG` cursor moves until the disk fills (#1526). Reading
 * the answer ourselves keeps the single piped answer @inquirer ever supported
 * working (`printf 'y\n' | openspec archive ...`) without emitting any escapes.
 *
 * `io` overrides the streams; it exists for tests and mirrors @inquirer's own
 * `{ input, output }` context. Production callers pass only the prompt.
 */
export async function confirmPrompt(prompt, io = {}) {
    const input = io.input ?? process.stdin;
    const output = io.output ?? process.stdout;
    const isTerminal = Boolean(input.isTTY) &&
        Boolean(output.isTTY);
    if (isTerminal) {
        const { confirm } = await import('@inquirer/prompts');
        return confirm(prompt);
    }
    return readYesNo(prompt, input, output);
}
function readYesNo(prompt, input, output) {
    return new Promise((resolve, reject) => {
        let settled = false;
        // Detach the input-stream error listener on settle: `input` is the
        // long-lived process.stdin, so a leftover listener would accumulate across
        // archive's successive prompts and swallow a later, unrelated stdin error.
        const cleanup = () => {
            input.removeListener('error', onError);
        };
        const blockOnNoAnswer = () => {
            if (settled)
                return;
            settled = true;
            cleanup();
            // No line could be read (stdin closed / EOF). Mirror @inquirer's failure
            // so callers that classify it — isNonInteractivePromptError, the #1479
            // "rerun with --yes" guidance — keep working unchanged.
            const error = new Error('User force closed the prompt');
            error.name = 'ExitPromptError';
            reject(error);
        };
        // A stdin error surfaces on the interface (readline forwards input-stream
        // errors since Node 16). Without a handler the promise would hang and the
        // 'error' would go unhandled; settle it with the real fault instead.
        const onError = (err) => {
            if (settled)
                return;
            settled = true;
            cleanup();
            rl.close();
            reject(err instanceof Error ? err : new Error(String(err)));
        };
        // An earlier prompt may have already drained stdin (only one piped answer
        // was ever supported). A fresh readline over an ended stream never emits
        // 'close', so guard here rather than hang and exit as a no-op.
        if (input.readableEnded) {
            blockOnNoAnswer();
            return;
        }
        output.write(`${prompt.message} ${prompt.default ? '(Y/n)' : '(y/N)'} `);
        // terminal:false guarantees readline never emits its own line-editing
        // escapes — an escape-free read is the whole point here.
        const rl = createInterface({ input, terminal: false });
        input.once('error', onError);
        rl.once('error', onError);
        rl.once('line', (line) => {
            if (settled)
                return;
            settled = true;
            cleanup();
            rl.close();
            output.write('\n');
            // Mirror @inquirer/confirm's parser (prefix match on y/yes and n/no,
            // otherwise the default) so a piped answer resolves identically to the
            // interactive prompt it replaces.
            const answer = line.trim();
            if (/^(y|yes)/i.test(answer)) {
                resolve(true);
            }
            else if (/^(n|no)/i.test(answer)) {
                resolve(false);
            }
            else {
                resolve(prompt.default);
            }
        });
        rl.once('close', () => {
            blockOnNoAnswer();
        });
    });
}
//# sourceMappingURL=interactive.js.map