import { existsSync, rmSync } from 'node:fs'; import { createInterface } from 'node:readline'; import { GATEWAY_HOME, getDaemonPid, readMeta, stopDaemon, uninstallGatewayPackage, } from './daemon.js'; export interface UninstallOptions { /** Skip the confirmation prompt (headless/scripted uninstall). */ yes?: boolean; /** Also remove all gateway data at GATEWAY_HOME (never implied by --yes). */ removeData?: boolean; } export async function runUninstall(opts: UninstallOptions = {}): Promise { const nonInteractive = Boolean(opts.yes) || process.env['MOSAIC_ASSUME_YES'] === '1'; // Non-TTY without explicit consent must FAIL LOUD, not quietly do nothing: // the pre-fix behavior (prompt on a closed stdin → default No → exit 0, // gateway untouched) reported success-by-silence to every scripted caller // (#1390). An explicit refusal beats a silent no-op. if (!nonInteractive && !process.stdin.isTTY) { console.error( 'gateway uninstall: stdin is not a TTY and no --yes was given — refusing to ' + 'run an interactive uninstall headlessly (nothing was changed). ' + 'Use --yes (and --remove-data to also delete gateway data), or run from a terminal.', ); process.exit(1); } const rl = nonInteractive ? null : createInterface({ input: process.stdin, output: process.stdout }); try { await doUninstall(rl as NonNullable, opts, nonInteractive); } finally { rl?.close(); } } function prompt( rl: NonNullable>, question: string, ): Promise { return new Promise((resolve) => rl.question(question, resolve)); } async function doUninstall( rl: ReturnType, opts: UninstallOptions, nonInteractive: boolean, ): Promise { const meta = readMeta(); if (!meta) { console.log('Gateway is not installed.'); return; } if (nonInteractive) { console.log(`Uninstalling Mosaic Gateway (--yes${opts.removeData ? ' --remove-data' : ''})...`); } else { const answer = await prompt(rl, 'Uninstall Mosaic Gateway? [y/N] '); if (answer.toLowerCase() !== 'y') { console.log('Aborted.'); return; } } // Stop if running if (getDaemonPid() !== null) { console.log('Stopping gateway daemon...'); try { await stopDaemon(); console.log('Stopped.'); } catch (err) { console.warn(`Warning: ${err instanceof Error ? err.message : String(err)}`); } } // Remove config/data. Interactive: ask. Headless: only with the explicit // flag — destructive recursion is never implied by --yes alone (#1390). let removeData = Boolean(opts.removeData); if (!nonInteractive) { const answer = await prompt(rl, `Remove all gateway data at ${GATEWAY_HOME}? [y/N] `); removeData = answer.toLowerCase() === 'y'; } if (removeData) { if (existsSync(GATEWAY_HOME)) { rmSync(GATEWAY_HOME, { recursive: true, force: true }); console.log('Gateway data removed.'); } } else { console.log(`Gateway data kept at ${GATEWAY_HOME}.`); } // Uninstall npm package console.log('Uninstalling npm package...'); uninstallGatewayPackage(); console.log('\nGateway uninstalled.'); }