#!/usr/bin/env node
/* eslint-disable */
/**
 * postinstall-daemon.cjs — system-service installer for the Omnius API daemon.
 *
 * Runs after `npm install -g omnius`. Responsibilities:
 *   1. Clean up stale nexus daemon files (preserving prior postinstall behaviour).
 *   2. Register a per-user system service that runs `omnius serve --daemon`
 *      on port 11435, surviving reboots/logins.
 *        Linux   → ~/.config/systemd/user/omnius-daemon.service
 *        macOS   → ~/Library/LaunchAgents/ai.omnius.daemon.plist
 *        Windows → Scheduled Task "OmniusDaemon" (onlogon)
 *   3. Start (or restart) the service so the daemon is live IMMEDIATELY —
 *      no need to launch `omnius` in any terminal.
 *   4. Never exit non-zero. npm install must not fail because of a service
 *      manager quirk or a permission quirk. We warn and move on.
 *
 * Opt-out: set env OMNIUS_SKIP_DAEMON_INSTALL=1 before `npm i -g`.
 *
 * ES5-safe pure CommonJS — runs on any Node version that survived preinstall.
 */

"use strict";

var os = require("os");
var path = require("path");
var fs = require("fs");
var cp = require("child_process");

var HOME = os.homedir();
var PLATFORM = os.platform(); // "linux", "darwin", "win32"
var IS_WIN = PLATFORM === "win32";
var IS_MAC = PLATFORM === "darwin";
var IS_LINUX = PLATFORM === "linux";

var PORT = parseInt(process.env.OMNIUS_PORT || "11435", 10);
var SERVICE_LABEL = "omnius-daemon";
var LAUNCHD_LABEL = "ai.omnius.daemon";
var WIN_TASK_NAME = "OmniusDaemon";
var REQUIRED_AIWG_VERSION = "2026.5.11";

// ─── Helpers ────────────────────────────────────────────────────────────────

function log(msg) {
  // Use stdout so npm captures it in the install log.
  process.stdout.write("  " + msg + "\n");
}
function warn(msg) {
  process.stdout.write("  [daemon] " + msg + "\n");
}
function quietInstall() {
  var level = String(process.env.npm_config_loglevel || "").toLowerCase();
  return process.env.OMNIUS_QUIET_INSTALL === "1" || level === "silent";
}
function progress(step, total, msg) {
  if (quietInstall()) return;
  var width = 18;
  var filled = Math.max(0, Math.min(width, Math.round((step / total) * width)));
  process.stdout.write("  [" + "#".repeat(filled) + ".".repeat(width - filled) + "] " + step + "/" + total + " " + msg + "\n");
}

function runQuiet(cmd, opts) {
  try {
    cp.execSync(cmd, Object.assign({ stdio: "pipe", timeout: 15000 }, opts || {}));
    return true;
  } catch (e) {
    return false;
  }
}

function runCapture(cmd, opts) {
  try {
    return cp.execSync(cmd, Object.assign({ stdio: ["ignore", "pipe", "ignore"], encoding: "utf8", timeout: 15000 }, opts || {})).trim();
  } catch (e) {
    return "";
  }
}

function hasCmd(name) {
  if (IS_WIN) return runQuiet("where " + name);
  return runQuiet("which " + name);
}

function shellQuote(value) {
  if (IS_WIN) return '"' + String(value).replace(/"/g, '\\"') + '"';
  return "'" + String(value).replace(/'/g, "'\\''") + "'";
}

function installedGlobalAiwgVersion() {
  try {
    var globalRoot = runCapture("npm root -g");
    if (globalRoot) {
      var pkgPath = path.join(globalRoot, "aiwg", "package.json");
      if (fs.existsSync(pkgPath)) {
        var pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
        if (pkg && pkg.version) return String(pkg.version);
      }
    }
  } catch (e) {}
  try {
    var out = runCapture("aiwg --version");
    var m = out && out.match(/(\d{4}\.\d+\.\d+(?:[-.][0-9A-Za-z]+)?)/);
    if (m) return m[1];
  } catch (e) {}
  return "";
}

function ensureGlobalAiwgInstall() {
  if (process.env.OMNIUS_SKIP_AIWG_INSTALL === "1") {
    log("OMNIUS_SKIP_AIWG_INSTALL=1 — skipping AIWG CLI install.");
    return;
  }
  if (!hasCmd("npm")) {
    warn("npm not found; cannot ensure aiwg@" + REQUIRED_AIWG_VERSION + ".");
    return;
  }
  var installed = installedGlobalAiwgVersion();
  if (installed === REQUIRED_AIWG_VERSION) {
    log("AIWG CLI already pinned: aiwg@" + REQUIRED_AIWG_VERSION);
    return;
  }
  var target = "aiwg@" + REQUIRED_AIWG_VERSION;
  log("Ensuring global AIWG CLI: " + target + (installed ? " (was " + installed + ")" : ""));
  try {
    cp.execSync(
      "npm install -g " + shellQuote(target) + " --prefer-online --cache-min=0 --no-audit --fund=false",
      { stdio: quietInstall() ? "pipe" : "inherit", timeout: 180000 }
    );
    log("AIWG CLI ready: " + target);
  } catch (e) {
    warn("Could not install " + target + " globally (non-fatal): " + (e && e.message ? e.message : String(e)));
    warn("Manual fix: npm install -g " + target);
  }
}

// Pre-check: is the port already serving an Omnius daemon?
function tryHealth(port, cb) {
  var http = require("http");
  var req = http.request(
    { host: "127.0.0.1", port: port, path: "/health", method: "GET", timeout: 1500 },
    function (res) {
      cb(res.statusCode === 200);
      res.resume();
    }
  );
  req.on("error", function () { cb(false); });
  req.on("timeout", function () { req.destroy(); cb(false); });
  req.end();
}

// Resolve the `omnius` launcher script.
//
// PRIORITY ORDER (most stable first):
//   1. Sibling launcher.cjs — guaranteed to live next to this postinstall,
//      survives across upgrades because it's part of the package itself.
//      Avoids the npx cache trap and the fnm shim trap.
//   2. npm-global bin (if npm_config_prefix is set) — stable across reboots.
//   3. PATH-resolved omnius — works in dev installs but can resolve to ephemeral
//      shims (npx cache, fnm multishell symlinks).
function resolveOmniusBinary() {
  var candidates = [];

  // 1. Self-relative — IDEAL: the launcher ships in the same directory as
  //    this postinstall script, so as long as the package is installed, it
  //    exists. This path is what should land in the systemd ExecStart.
  try {
    candidates.push(path.resolve(__dirname, "launcher.cjs"));
  } catch (e) {}

  // 2. npm prefix bin
  var prefix = process.env.npm_config_prefix || "";
  if (!prefix) prefix = runCapture("npm prefix -g");
  if (prefix) {
    if (IS_WIN) {
      candidates.push(path.join(prefix, "omnius.cmd"));
      candidates.push(path.join(prefix, "omnius"));
    } else {
      candidates.push(path.join(prefix, "bin", "omnius"));
      candidates.push(path.join(prefix, "bin", "omnius"));
    }
  }
  // 3. PATH fallback (last resort — can resolve to npx cache or fnm shim).
  var found = runCapture(IS_WIN ? "where omnius" : "which omnius").split(/\r?\n/)[0];
  if (found && !/_npx|fnm_multishells/.test(found)) candidates.push(found);

  for (var i = 0; i < candidates.length; i++) {
    try {
      if (candidates[i] && fs.existsSync(candidates[i])) return candidates[i];
    } catch (e) {}
  }
  return null;
}

// Find a Node binary we can use in the service ExecStart.
// On nvm systems this is important — /usr/bin/node is often too old.
function resolveNodeBinary() {
  try {
    if (process.execPath && fs.existsSync(process.execPath)) return process.execPath;
  } catch (e) {}
  var found = runCapture(IS_WIN ? "where node" : "which node").split(/\r?\n/)[0];
  if (found) return found;
  return "node";
}

// Effective user for service ownership. Handle `sudo npm i -g` by preferring
// SUDO_USER when running as root on Linux/macOS.
function effectiveUser() {
  if (!IS_WIN && process.getuid && process.getuid() === 0) {
    var sudoUser = process.env.SUDO_USER;
    if (sudoUser) return sudoUser;
    return null; // Running as bare root — skip per-user service install
  }
  return process.env.USER || process.env.LOGNAME || os.userInfo().username;
}

// ─── Force-kill port holder (regardless of how it was launched) ────────────
//
// Critical for upgrade correctness: when an OLD daemon (started before this
// install) is running, it holds port 11435 with stale in-memory code. The
// systemctl/launchctl `restart` calls below CANNOT reach it because the
// service manager doesn't own that process — it was started detached by
// `omnius serve --daemon` from a previous TUI session.
//
// Result without this kill: postinstall tries to start a NEW systemd
// daemon, port-bind fails, old daemon stays alive serving stale code,
// and the user's runs continue with broken patches.
//
// Strategy (matches packages/cli/src/daemon.ts:forceKillDaemon):
//   1. SIGTERM via known PID files (~/.omnius/daemon.pid + .omnius/nexus/daemon.pid)
//   2. lsof / fuser port probe — find ANY other holder
//   3. 2s graceful grace, then SIGKILL stragglers
//   4. Poll /health up to 5s for confirmation
//
// Skips when OMNIUS_DISABLE_FORCE_KILL_DAEMON=1 (escape valve).
function forceKillPortHolder(port, cb) {
  if (process.env.OMNIUS_DISABLE_FORCE_KILL_DAEMON === "1") {
    log("OMNIUS_DISABLE_FORCE_KILL_DAEMON=1 — skipping port-holder kill (upgrades may not pick up new code).");
    return cb(0);
  }

  var killed = 0;

  // Step 1: SIGTERM via PID files (graceful)
  var pidFiles = [
    path.join(HOME, ".omnius", "daemon.pid"),
    path.join(process.cwd(), ".omnius", "nexus", "daemon.pid"),
  ];
  pidFiles.forEach(function (pidFile) {
    try {
      if (!fs.existsSync(pidFile)) return;
      var n = parseInt(fs.readFileSync(pidFile, "utf8").trim(), 10);
      if (!n || n <= 0) return;
      try { process.kill(n, "SIGTERM"); killed++; log("SIGTERM old daemon (pid " + n + ", from " + pidFile + ")"); } catch (e) { /* dead */ }
    } catch (e) { /* */ }
  });

  // Step 2: port probe — lsof/fuser to find ANY pid holding the port
  try {
    var out = "";
    try {
      out = cp.execSync("lsof -ti :" + port + " 2>/dev/null || fuser " + port + "/tcp 2>/dev/null || true", {
        encoding: "utf8", timeout: 3000,
      }).trim();
    } catch (e) { /* tools unavailable */ }
    if (out) {
      var pids = out.split(/[\s\n]+/).map(function (s) { return parseInt(s, 10); }).filter(function (n) {
        return Number.isFinite(n) && n > 0 && n !== process.pid;
      });
      pids.forEach(function (otherPid) {
        try { process.kill(otherPid, "SIGTERM"); killed++; log("SIGTERM port-holder (pid " + otherPid + ")"); } catch (e) { /* */ }
      });
    }
  } catch (e) { /* */ }

  if (killed === 0) {
    // Nothing to kill — port was already free.
    return cb(0);
  }

  // Step 3: 2s grace, then SIGKILL stragglers
  setTimeout(function () {
    try {
      var out = cp.execSync("lsof -ti :" + port + " 2>/dev/null || fuser " + port + "/tcp 2>/dev/null || true", {
        encoding: "utf8", timeout: 3000,
      }).trim();
      if (out) {
        var pids = out.split(/[\s\n]+/).map(function (s) { return parseInt(s, 10); }).filter(function (n) {
          return Number.isFinite(n) && n > 0 && n !== process.pid;
        });
        pids.forEach(function (otherPid) {
          try { process.kill(otherPid, "SIGKILL"); log("SIGKILL straggler (pid " + otherPid + ")"); } catch (e) { /* */ }
        });
      }
    } catch (e) { /* */ }

    // Step 4: poll until port is free (up to 5s) — required so the next
    // service-manager restart binds without conflict.
    var pollStart = Date.now();
    function pollFree() {
      if (Date.now() - pollStart > 5000) return cb(killed);
      tryHealth(port, function (alive) {
        if (!alive) return cb(killed);
        setTimeout(pollFree, 200);
      });
    }
    pollFree();

    // Clean up PID files now that the processes are dead
    pidFiles.forEach(function (pidFile) {
      try { if (fs.existsSync(pidFile)) fs.unlinkSync(pidFile); } catch (e) { /* */ }
    });
  }, 2000);
}

// ─── Nexus cleanup (preserve prior postinstall behaviour) ──────────────────

function cleanNexus() {
  try {
    var dirs = [
      path.join(HOME, ".omnius", "nexus"),
      path.join(process.cwd(), ".omnius", "nexus"),
    ];
    dirs.forEach(function (d) {
      var stale = path.join(d, "nexus-daemon.mjs");
      try {
        if (fs.existsSync(stale)) {
          fs.unlinkSync(stale);
          log("Cleaned stale nexus daemon: " + stale);
        }
      } catch (e) {}
      var pidFile = path.join(d, "daemon.pid");
      try {
        if (fs.existsSync(pidFile)) {
          var n = parseInt(fs.readFileSync(pidFile, "utf8"), 10);
          if (n > 0) {
            try { process.kill(n, "SIGTERM"); } catch (e) {}
          }
          fs.unlinkSync(pidFile);
          log("Killed old nexus daemon: PID " + n);
        }
      } catch (e) {}
    });
  } catch (e) {}
}

// ─── Linux: systemd user unit ───────────────────────────────────────────────

function installSystemd(nodeBin, omniusScript, user) {
  if (!IS_LINUX) return false;
  if (!hasCmd("systemctl")) {
    warn("systemctl not found — skipping systemd install.");
    return false;
  }

  var userHome = user === process.env.USER ? HOME : path.join("/home", user);
  try {
    var pwdEntry = runCapture("getent passwd " + user);
    if (pwdEntry) {
      var parts = pwdEntry.split(":");
      if (parts[5]) userHome = parts[5];
    }
  } catch (e) {}

  var unitDir = path.join(userHome, ".config", "systemd", "user");
  var unitPath = path.join(unitDir, SERVICE_LABEL + ".service");
  var logDir = path.join(userHome, ".omnius");

  try { fs.mkdirSync(unitDir, { recursive: true }); } catch (e) {}
  try { fs.mkdirSync(logDir, { recursive: true }); } catch (e) {}

  // The unit runs: <node> <omniusScript> serve --daemon --quiet
  // with OMNIUS_DAEMON=1 so the serve command picks the daemon path.
  var unit = [
    "[Unit]",
    "Description=Omnius API Daemon (port " + PORT + ")",
    "Documentation=https://github.com/robit-man/omnius",
    "After=network-online.target",
    "Wants=network-online.target",
    "",
    "[Service]",
    "Type=simple",
    "Environment=OMNIUS_DAEMON=1",
    "Environment=OMNIUS_PORT=" + PORT,
    "Environment=NODE_ENV=production",
    "ExecStart=" + nodeBin + " " + omniusScript + " serve --daemon --quiet",
    // Restart=always (was on-failure) — also relaunch on clean exit.
    // Some upgrade flows trigger process.exit(0) (e.g. /update reload,
    // SIGTERM during npm install), which Restart=on-failure ignores.
    // Pair with StartLimitIntervalSec to prevent thrash loops.
    "Restart=always",
    "RestartSec=3",
    "StartLimitIntervalSec=30",
    "StartLimitBurst=10",
    "StandardOutput=append:" + path.join(logDir, "daemon.log"),
    "StandardError=append:" + path.join(logDir, "daemon.err.log"),
    "",
    "[Install]",
    "WantedBy=default.target",
    "",
  ].join("\n");

  try {
    fs.writeFileSync(unitPath, unit, { encoding: "utf8" });
    log("Wrote systemd user unit: " + unitPath);
  } catch (e) {
    warn("Failed to write systemd unit: " + (e && e.message));
    return false;
  }

  // If we're root installing for another user, we need to wrap systemctl
  // calls in `sudo -u USER XDG_RUNTIME_DIR=/run/user/UID systemctl --user`.
  var asUserPrefix = "";
  if (!IS_WIN && process.getuid && process.getuid() === 0 && user !== "root") {
    // Try to resolve UID for the target user.
    var uid = runCapture("id -u " + user);
    if (uid && /^\d+$/.test(uid)) {
      // Ensure loginctl linger so user services can run without a session.
      runQuiet("loginctl enable-linger " + user);
      asUserPrefix = "sudo -u " + user + " XDG_RUNTIME_DIR=/run/user/" + uid + " DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/" + uid + "/bus ";
    }
  } else if (!IS_WIN && process.getuid && process.getuid() !== 0) {
    // Self-install: enable-linger ourselves if we can (best-effort; needs sudo on most distros).
    runQuiet("loginctl enable-linger " + user);
  }

  var sysctl = asUserPrefix + "systemctl --user";
  runQuiet(sysctl + " daemon-reload");
  var enabled = runQuiet(sysctl + " enable " + SERVICE_LABEL + ".service");
  var restarted = runQuiet(sysctl + " restart " + SERVICE_LABEL + ".service");

  if (enabled) log("Enabled systemd unit: " + SERVICE_LABEL);
  if (restarted) log("Started systemd unit: " + SERVICE_LABEL);
  if (!enabled || !restarted) {
    warn("Could not enable/start the unit via systemctl --user. The file is in place; run manually:");
    warn("  systemctl --user daemon-reload");
    warn("  systemctl --user enable --now " + SERVICE_LABEL + ".service");
  }

  return true;
}

// ─── macOS: launchd user agent ──────────────────────────────────────────────

function installLaunchd(nodeBin, omniusScript, user) {
  if (!IS_MAC) return false;

  var userHome = HOME;
  if (user && user !== os.userInfo().username) {
    try {
      var home = runCapture("dscl . -read /Users/" + user + " NFSHomeDirectory").split(" ").pop();
      if (home) userHome = home;
    } catch (e) {}
  }

  var agentDir = path.join(userHome, "Library", "LaunchAgents");
  var plistPath = path.join(agentDir, LAUNCHD_LABEL + ".plist");
  var logDir = path.join(userHome, ".omnius");

  try { fs.mkdirSync(agentDir, { recursive: true }); } catch (e) {}
  try { fs.mkdirSync(logDir, { recursive: true }); } catch (e) {}

  var plist = [
    '<?xml version="1.0" encoding="UTF-8"?>',
    '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
    '<plist version="1.0">',
    '<dict>',
    '  <key>Label</key><string>' + LAUNCHD_LABEL + '</string>',
    '  <key>ProgramArguments</key>',
    '  <array>',
    '    <string>' + nodeBin + '</string>',
    '    <string>' + omniusScript + '</string>',
    '    <string>serve</string>',
    '    <string>--daemon</string>',
    '    <string>--quiet</string>',
    '  </array>',
    '  <key>EnvironmentVariables</key>',
    '  <dict>',
    '    <key>OMNIUS_DAEMON</key><string>1</string>',
    '    <key>OMNIUS_PORT</key><string>' + PORT + '</string>',
    '    <key>PATH</key><string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>',
    '  </dict>',
    '  <key>RunAtLoad</key><true/>',
    '  <key>KeepAlive</key><true/>',
    '  <key>StandardOutPath</key><string>' + path.join(logDir, "daemon.log") + '</string>',
    '  <key>StandardErrorPath</key><string>' + path.join(logDir, "daemon.err.log") + '</string>',
    '</dict>',
    '</plist>',
    '',
  ].join("\n");

  try {
    fs.writeFileSync(plistPath, plist, { encoding: "utf8" });
    log("Wrote launchd plist: " + plistPath);
  } catch (e) {
    warn("Failed to write launchd plist: " + (e && e.message));
    return false;
  }

  // Unload then load (idempotent). bootout/bootstrap prefer gui/<uid> on newer macOS.
  var uid = runCapture("id -u " + user);
  if (uid && /^\d+$/.test(uid)) {
    runQuiet("launchctl bootout gui/" + uid + "/" + LAUNCHD_LABEL);
    var booted = runQuiet("launchctl bootstrap gui/" + uid + " " + plistPath);
    if (booted) log("Loaded launchd agent: " + LAUNCHD_LABEL);
    runQuiet("launchctl kickstart -k gui/" + uid + "/" + LAUNCHD_LABEL);
  } else {
    runQuiet("launchctl unload " + plistPath);
    runQuiet("launchctl load -w " + plistPath);
  }
  return true;
}

// ─── Windows: Scheduled Task ────────────────────────────────────────────────

function installWindows(nodeBin, omniusScript) {
  if (!IS_WIN) return false;
  if (!hasCmd("schtasks")) {
    warn("schtasks not found — skipping Windows scheduled task install.");
    return false;
  }

  // Build the task command — quote the paths.
  var trCmd = '"' + nodeBin + '" "' + omniusScript + '" serve --daemon --quiet';

  // Delete any existing task first (idempotent).
  runQuiet('schtasks /Delete /TN "' + WIN_TASK_NAME + '" /F');

  var created = runQuiet(
    'schtasks /Create /F /SC ONLOGON /RL HIGHEST /TN "' + WIN_TASK_NAME + '" /TR ' + JSON.stringify(trCmd)
  );
  if (!created) {
    warn("Failed to create scheduled task. Try running `omnius daemon install` from an elevated shell.");
    return false;
  }
  log("Created scheduled task: " + WIN_TASK_NAME);
  // Start it immediately.
  runQuiet('schtasks /Run /TN "' + WIN_TASK_NAME + '"');
  return true;
}

// ─── Fallback: spawn detached now (no persistence) ─────────────────────────

function spawnDetached(nodeBin, omniusScript) {
  try {
    var child = cp.spawn(nodeBin, [omniusScript, "serve", "--daemon", "--quiet"], {
      detached: true,
      stdio: "ignore",
      env: Object.assign({}, process.env, { OMNIUS_DAEMON: "1", OMNIUS_PORT: String(PORT) }),
    });
    child.unref();
    if (child.pid) {
      log("Spawned detached daemon (PID " + child.pid + "). It will NOT survive reboot — install a service manager to persist.");
      return true;
    }
  } catch (e) {
    warn("Failed to spawn detached daemon: " + (e && e.message));
  }
  return false;
}

// ─── Wait for /health ───────────────────────────────────────────────────────

function waitForHealth(timeoutMs, cb) {
  var start = Date.now();
  function tick() {
    tryHealth(PORT, function (ok) {
      if (ok) return cb(true);
      if (Date.now() - start > timeoutMs) return cb(false);
      setTimeout(tick, 500);
    });
  }
  tick();
}

// ─── Main ───────────────────────────────────────────────────────────────────

// ─── Wrapper-modelfile self-heal ───────────────────────────────────────────
// Old Omnius versions wrote `FROM <blob-path>` into omnius-* wrappers,
// which strips TEMPLATE/RENDERER/PARSER metadata and breaks tools support.
// On upgrade, scan local Ollama for stale wrappers and rebuild them with
// `FROM <baseModel>`. Best-effort, silent on failure — postinstall must not
// fail npm install.
function repairBrokenWrappers() {
  // Independent opt-out from daemon install — wrapper repair is a different
  // concern and runs even when OMNIUS_SKIP_DAEMON_INSTALL=1, unless explicitly
  // disabled.
  if (process.env.OMNIUS_SKIP_WRAPPER_REPAIR === "1") return;
  // Skip if ollama isn't on PATH.
  var ollamaBin = "";
  try {
    ollamaBin = cp.execSync("command -v ollama 2>/dev/null || which ollama 2>/dev/null", {
      encoding: "utf8", stdio: "pipe", timeout: 5000,
    }).trim();
  } catch (e) { /* ignore */ }
  if (!ollamaBin) return;

  // List models. Format is one per line: NAME ID SIZE MODIFIED.
  var listOut = "";
  try {
    listOut = cp.execSync(ollamaBin + " list 2>/dev/null", {
      encoding: "utf8", stdio: "pipe", timeout: 8000,
    });
  } catch (e) { return; }
  var lines = listOut.split(/\r?\n/).filter(function (l) { return l.trim().length > 0; });
  // Skip header row if present.
  if (lines.length > 0 && /^NAME\s/i.test(lines[0])) lines = lines.slice(1);
  var allNames = lines.map(function (l) { return l.split(/\s+/)[0]; });
  var wrappers = allNames.filter(function (n) { return /^omnius-/i.test(n); });
  var bases = allNames.filter(function (n) { return !/^omnius-/i.test(n); });
  if (wrappers.length === 0) return;

  function expandedName(base) {
    // Mirror packages/cli/src/tui/setup.ts:expandedModelName.
    var canonical = base.replace(/:latest$/i, "");
    return "omnius-" + canonical.replace(":", "-").replace(/\./g, "");
  }
  function legacyExpandedName(base) {
    return "omnius-" + base.replace(":", "-").replace(/\./g, "");
  }
  function stripTag(n) { return (n || "").replace(/:[^:]+$/, ""); }
  function guessBase(wrapper) {
    var stripped = stripTag(wrapper);
    // Try the FULL base name first (e.g. "qwen3.6:27b") so size-tagged
    // variants don't collide with each other. expandedName/legacyExpandedName
    // handle the tag internally.
    for (var i = 0; i < bases.length; i++) {
      var name = bases[i];
      if (expandedName(name) === stripped || legacyExpandedName(name) === stripped) {
        return name;
      }
    }
    // Then fall back to stripped-tag base — this is the only way to match
    // wrappers that were built from `<base>:latest` and got their canonical
    // form (`omnius-<base-stripped>`). Prefer bases tagged ":latest"
    // so that ambiguity (qwen3.6:27b vs qwen3.6:35b vs qwen3.6:latest)
    // resolves to the user-facing default.
    var latestFirst = bases.slice().sort(function (a, b) {
      var aL = /:latest$/i.test(a) ? 0 : 1;
      var bL = /:latest$/i.test(b) ? 0 : 1;
      return aL - bL;
    });
    for (var j = 0; j < latestFirst.length; j++) {
      var basis = stripTag(latestFirst[j]);
      if (expandedName(basis) === stripped || legacyExpandedName(basis) === stripped) {
        return latestFirst[j];
      }
    }
    return null;
  }
  function showCapabilities(name) {
    try {
      var out = cp.execSync(ollamaBin + " show " + name + " 2>/dev/null", {
        encoding: "utf8", stdio: "pipe", timeout: 8000,
      });
      // Parse capabilities section
      var inCaps = false;
      var caps = [];
      var rows = out.split(/\r?\n/);
      for (var i = 0; i < rows.length; i++) {
        var t = rows[i];
        if (/^\s*Capabilities\s*$/.test(t)) { inCaps = true; continue; }
        if (inCaps) {
          if (/^\s*[A-Z][a-z]/.test(t) || t.trim() === "") { inCaps = false; continue; }
          var v = t.trim();
          if (v) caps.push(v.toLowerCase());
        }
      }
      return caps;
    } catch (e) { return null; }
  }
  function modelfileLooksBlob(name) {
    try {
      var mf = cp.execSync(ollamaBin + " show --modelfile " + name + " 2>/dev/null", {
        encoding: "utf8", stdio: "pipe", timeout: 8000,
      });
      var m = mf.match(/^FROM\s+(.+)$/m);
      if (!m) return false;
      var f = m[1].trim();
      return f.charAt(0) === "/" || /blobs\/sha256[-:]/.test(f);
    } catch (e) { return false; }
  }

  var modelDir = path.join(HOME, ".omnius", "models");
  try { fs.mkdirSync(modelDir, { recursive: true }); } catch (e) {}

  var rebuilt = 0;
  var skipped = 0;
  for (var i = 0; i < wrappers.length; i++) {
    var w = wrappers[i];
    var caps = showCapabilities(w);
    var hasTools = caps !== null ? caps.indexOf("tools") !== -1 : true;
    var blobby = modelfileLooksBlob(w);
    if (hasTools && !blobby) { skipped++; continue; }
    var base = guessBase(w);
    if (!base) { skipped++; continue; }

    // Read current num_ctx so we don't shrink it below what's already baked.
    var numCtx = 32768;
    try {
      var pa = cp.execSync(ollamaBin + " show --parameters " + w + " 2>/dev/null", {
        encoding: "utf8", stdio: "pipe", timeout: 5000,
      });
      var nm = pa.match(/num_ctx\s+(\d+)/);
      if (nm) numCtx = Math.max(numCtx, parseInt(nm[1], 10));
    } catch (e) {}
    var numPredict = Math.min(16384, Math.max(2048, Math.floor(numCtx * 0.25)));
    var content =
      "FROM " + base + "\n" +
      "PARAMETER num_ctx " + numCtx + "\n" +
      "PARAMETER temperature 0\n" +
      "PARAMETER num_predict " + numPredict + "\n" +
      'PARAMETER stop "<|endoftext|>"\n';
    var mfPath = path.join(modelDir, "Modelfile." + stripTag(w));
    try { fs.writeFileSync(mfPath, content, "utf8"); } catch (e) { skipped++; continue; }

    try {
      cp.execSync(ollamaBin + " create " + stripTag(w) + " -f " + JSON.stringify(mfPath) + " 2>&1", {
        stdio: "pipe", timeout: 180000,
      });
      log("repaired wrapper: " + w + " (FROM " + base + ", num_ctx=" + numCtx + ")");
      rebuilt++;
    } catch (e) {
      warn("failed to rebuild " + w + ": " + (e && e.message ? e.message.slice(0, 200) : "unknown"));
      skipped++;
    }
  }

  if (rebuilt > 0) {
    log("Rebuilt " + rebuilt + " stale Omnius wrapper(s); " + skipped + " unchanged.");
  }
}

function main() {
  // Always do the nexus cleanup first, regardless of opt-out.
  progress(1, 6, "Cleaning stale Nexus runtime state");
  cleanNexus();

  // Auto-repair stale Omnius model wrappers BEFORE restarting the daemon, so the
  // freshly-restarted daemon picks up the rebuilt models on first inference.
  progress(2, 6, "Checking local model wrappers");
  try { repairBrokenWrappers(); } catch (e) {
    warn("wrapper auto-repair crashed (non-fatal): " + (e && e.message));
  }

  progress(3, 6, "Ensuring AIWG CLI");
  ensureGlobalAiwgInstall();

  if (process.env.OMNIUS_SKIP_DAEMON_INSTALL === "1") {
    log("OMNIUS_SKIP_DAEMON_INSTALL=1 — skipping daemon service install.");
    return safeExit(0);
  }

  // Force-kill any process holding the daemon port BEFORE we try to install
  // a service. This handles the orphan-detached-daemon case (started by
  // `omnius serve --daemon` from a previous TUI session) — systemctl/launchctl
  // restart can't reach it, so we have to clean up explicitly. Without this,
  // the new service-managed daemon fails to bind port 11435 and the user
  // ends up running stale in-memory code from the previous version.
  progress(4, 6, "Clearing daemon port");
  forceKillPortHolder(PORT, function (killedCount) {
    if (killedCount > 0) {
      log("Killed " + killedCount + " stale daemon process(es) holding port " + PORT + ".");
    }
    runMainAfterKill();
  });
}

function runMainAfterKill() {
  // Fast path: daemon already answering /health on the target port.
  // After forceKillPortHolder, this should be false unless a service manager
  // already auto-restarted (rare race). We still check so the rest of the
  // flow stays idempotent.
  tryHealth(PORT, function (alreadyUp) {
    var user = effectiveUser();
    var nodeBin = resolveNodeBinary();
    var omniusScript = resolveOmniusBinary();

    if (!omniusScript) {
      warn("Could not resolve `omnius` launcher — daemon install skipped. After install completes, run `omnius daemon install` manually.");
      return safeExit(0);
    }

    if (alreadyUp) {
      log("Omnius daemon answering /health on port " + PORT + " — re-checking version drift before service (re)install.");
      // Note: we already force-killed any stale process — anything answering
      // now is a fresh service-managed daemon. Service (re)install below is
      // idempotent and ensures the unit file matches the current bundle.
    }

    progress(5, 6, "Installing daemon service");
    log("Installing Omnius API daemon service for port " + PORT + " ...");
    log("  node:       " + nodeBin);
    log("  omnius script:  " + omniusScript);
    log("  user:       " + (user || "(unknown)"));

    if (!user) {
      warn("Running as bare root with no SUDO_USER — skipping per-user service install.");
      warn("Re-run as your user, or run: OMNIUS_SKIP_DAEMON_INSTALL=0 npm i -g omnius");
      // Still spawn detached so the port is live right now.
      spawnDetached(nodeBin, omniusScript);
      return safeExit(0);
    }

    var ok = false;
    if (IS_LINUX) {
      ok = installSystemd(nodeBin, omniusScript, user);
    } else if (IS_MAC) {
      ok = installLaunchd(nodeBin, omniusScript, user);
    } else if (IS_WIN) {
      ok = installWindows(nodeBin, omniusScript);
    } else {
      warn("Unsupported platform: " + PLATFORM + " — falling back to detached spawn.");
    }

    if (!ok) {
      // Fallback: spawn a detached child so at least the port is live NOW.
      spawnDetached(nodeBin, omniusScript);
    }

    // Wait up to 15s for /health to come up, but don't fail npm install.
    progress(6, 6, "Verifying daemon health");
    waitForHealth(15000, function (healthy) {
      if (healthy) {
        log("Omnius API daemon is live: http://127.0.0.1:" + PORT + "/health");
      } else {
        warn("Omnius API daemon did not answer /health within 15s. Check logs:");
        if (IS_LINUX) warn("  systemctl --user status " + SERVICE_LABEL);
        if (IS_MAC) warn("  launchctl print gui/$(id -u)/" + LAUNCHD_LABEL);
        if (IS_WIN) warn("  schtasks /Query /TN " + WIN_TASK_NAME + " /V /FO LIST");
      }
      safeExit(0);
    });
  });
}

function safeExit(code) {
  // Never exit non-zero from postinstall — it breaks `npm i -g`.
  process.exit(typeof code === "number" ? 0 : 0);
}

// Guard against any unhandled throw — postinstall must not crash npm.
try {
  main();
} catch (e) {
  warn("postinstall crashed (non-fatal): " + (e && e.message));
  safeExit(0);
}
