#!/usr/bin/env node
import { spawn } from 'child_process';
import { readFile, mkdir, open, rename, unlink } from 'fs/promises';
import { dirname, join } from 'path';
import { randomBytes } from 'crypto';
import { z } from 'zod';
import lockfile from 'proper-lockfile';

function homePaths(home) {
  const state = join(home, "state");
  const config = join(home, "config");
  return {
    home,
    commons: join(home, "commons"),
    commonsReference: join(home, "commons", "reference"),
    commonsScratch: join(home, "commons", "scratch"),
    agents: join(home, "agents"),
    sharedBrain: join(home, "shared", "brain"),
    sharedBrainIndex: join(state, "brain", "__shared__", "brain.db"),
    state,
    stateSupervisorJson: join(state, "supervisor.json"),
    stateSupervisorSock: join(state, "supervisor.sock"),
    stateSupervisorPid: join(state, "supervisor.pid"),
    stateSupervisorLog: join(state, "supervisor.log"),
    stateNotifications: join(state, "notifications"),
    stateCredentialRequests: join(state, "credential-requests"),
    stateOpenpub: join(state, "openpub"),
    stateTelemetry: join(state, "telemetry"),
    stateWebTokens: join(state, "web-tokens"),
    config,
    configUserMd: join(config, "user.md"),
    configUserPubSecret: join(config, "user.pub.secret"),
    configEndpoints: join(config, "endpoints.json")
  };
}
async function atomicWriteFile(path, data) {
  const tmpSuffix = randomBytes(6).toString("hex");
  const tmpPath = `${path}.tmp.${tmpSuffix}`;
  let handle;
  try {
    handle = await open(tmpPath, "wx");
    await handle.writeFile(data);
    await handle.sync();
    await handle.close();
    handle = void 0;
    await rename(tmpPath, path);
  } catch (err) {
    if (handle) {
      try {
        await handle.close();
      } catch {
      }
    }
    try {
      await unlink(tmpPath);
    } catch {
    }
    throw err;
  }
}
async function atomicWriteJson(path, data) {
  await atomicWriteFile(path, JSON.stringify(data, null, 2) + "\n");
}
var UpgradeStages = [
  "pending",
  "stopping_daemon",
  "installing",
  "restarting",
  "completed",
  "failed"
];
var UpgradeStatusSchema = z.object({
  schema_version: z.literal(1),
  stage: z.enum(UpgradeStages),
  version_from: z.string(),
  version_to: z.string(),
  triggered_at: z.string(),
  // ISO8601
  updated_at: z.string(),
  // ISO8601, advances per stage
  finished_at: z.string().nullable(),
  // set when stage is 'completed' or 'failed'
  error: z.string().nullable()
  // human-readable; populated on failed
});
function upgradeStatusPath(home) {
  return join(homePaths(home).state, "upgrade-status.json");
}
async function readUpgradeStatus(home) {
  const path = upgradeStatusPath(home);
  let raw;
  try {
    raw = await readFile(path, "utf8");
  } catch (err) {
    if (isEnoent(err)) return null;
    throw err;
  }
  const parsed = JSON.parse(raw);
  return UpgradeStatusSchema.parse(parsed);
}
async function writeUpgradeStatus(home, status) {
  const path = upgradeStatusPath(home);
  await mkdir(dirname(path), { recursive: true });
  await atomicWriteJson(path, status);
}
async function advanceUpgradeStage(home, stage, opts = {}) {
  const current = await readUpgradeStatus(home);
  if (current === null) {
    throw new Error(
      `cannot advance upgrade-status: no current status at ${upgradeStatusPath(home)}`
    );
  }
  const now = (/* @__PURE__ */ new Date()).toISOString();
  const next = {
    ...current,
    stage,
    updated_at: now,
    finished_at: stage === "completed" || stage === "failed" ? now : current.finished_at,
    error: opts.error ?? current.error
  };
  await writeUpgradeStatus(home, next);
  return next;
}
function isEnoent(err) {
  return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
}
var STALE_MS = 1e4;
async function isLockHeld(filePath) {
  try {
    return await lockfile.check(filePath, {
      stale: STALE_MS,
      realpath: false
    });
  } catch (err) {
    if (isEnoent2(err)) return false;
    throw err;
  }
}
function isEnoent2(err) {
  return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
}

// src/runtime/supervisor/pidfile.ts
function pidFilePath(home) {
  return homePaths(home).stateSupervisorPid;
}
function isPidAlive(pid) {
  if (!Number.isInteger(pid) || pid <= 0) return false;
  try {
    process.kill(pid, 0);
    return true;
  } catch (err) {
    const code = err?.code;
    if (code === "ESRCH") return false;
    return true;
  }
}

// src/runtime/install/upgrade-runner.ts
var POLL_INTERVAL_MS = 250;
var DAEMON_SHUTDOWN_TIMEOUT_MS = 6e4;
var DAEMON_BOOT_TIMEOUT_MS = 3e4;
async function main() {
  const cfg = resolveConfig();
  await advanceUpgradeStage(cfg.home, "stopping_daemon");
  const daemonStopped = await waitForDaemonExit(cfg.home, cfg.daemonPid, DAEMON_SHUTDOWN_TIMEOUT_MS);
  if (!daemonStopped) {
    await fail(
      cfg,
      `daemon (pid ${String(cfg.daemonPid)}) did not exit within ${String(DAEMON_SHUTDOWN_TIMEOUT_MS)}ms`
    );
    return;
  }
  await advanceUpgradeStage(cfg.home, "installing");
  const installResult = await runNpmInstall(cfg.packageName, cfg.targetVersion);
  if (installResult.code !== 0) {
    await fail(
      cfg,
      `npm install exited ${String(installResult.code)}:
${installResult.stderr.slice(0, 1e3)}`
    );
    await tryRestartDaemon(cfg).catch(() => {
    });
    return;
  }
  await advanceUpgradeStage(cfg.home, "restarting");
  const restarted = await tryRestartDaemon(cfg);
  if (!restarted) {
    await fail(cfg, "package installed but the new daemon did not start; check supervisor.log");
    return;
  }
  await advanceUpgradeStage(cfg.home, "completed");
}
function resolveConfig() {
  const home = requireEnv("TWENTYTWOHUNDRED_HOME");
  const daemonPidStr = requireEnv("TWENTYTWOHUNDRED_DAEMON_PID");
  const targetVersion = requireEnv("TWENTYTWOHUNDRED_TARGET_VERSION");
  const packageName = process.env["TWENTYTWOHUNDRED_PACKAGE_NAME"] ?? "@twentytwohundred/2200-cli";
  const daemonPid = Number.parseInt(daemonPidStr, 10);
  if (!Number.isFinite(daemonPid) || daemonPid <= 0) {
    throw new Error(`TWENTYTWOHUNDRED_DAEMON_PID is not a positive integer: ${daemonPidStr}`);
  }
  return { home, daemonPid, targetVersion, packageName };
}
function requireEnv(name) {
  const v = process.env[name];
  if (v === void 0 || v.length === 0) {
    throw new Error(`upgrade-runner: required env var ${name} is not set`);
  }
  return v;
}
async function waitForDaemonExit(home, pid, timeoutMs) {
  const lockPath = pidFilePath(home);
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    if (!await isLockHeld(lockPath)) return true;
    if (!isPidAlive(pid)) return true;
    await sleep(POLL_INTERVAL_MS);
  }
  return false;
}
async function runNpmInstall(packageName, version) {
  return new Promise((resolve) => {
    const child = spawn("npm", ["install", "-g", `${packageName}@${version}`], {
      stdio: ["ignore", "ignore", "pipe"]
    });
    let stderr = "";
    child.stderr.on("data", (chunk) => {
      stderr += chunk.toString("utf8");
    });
    child.on("error", (err) => {
      stderr += `
spawn error: ${err.message}`;
      resolve({ code: 127, stderr });
    });
    child.on("exit", (code) => {
      resolve({ code: code ?? 1, stderr });
    });
  });
}
async function tryRestartDaemon(cfg) {
  const prefix = await getNpmPrefix();
  if (prefix === null) return false;
  const binaryPath = join(prefix, "bin", "2200");
  const child = spawn(binaryPath, ["daemon", "start"], {
    env: { ...process.env, TWENTYTWOHUNDRED_HOME: cfg.home },
    stdio: "ignore",
    detached: true
  });
  child.unref();
  child.on("error", () => {
  });
  const pidPath = join(homePaths(cfg.home).state, "supervisor.pid");
  const deadline = Date.now() + DAEMON_BOOT_TIMEOUT_MS;
  while (Date.now() < deadline) {
    const livePid = await readLivePidFile(pidPath);
    if (livePid !== null) return true;
    await sleep(POLL_INTERVAL_MS);
  }
  return false;
}
async function readLivePidFile(path) {
  let raw;
  try {
    raw = await readFile(path, "utf8");
  } catch {
    return null;
  }
  const n = Number.parseInt(raw.trim(), 10);
  if (!Number.isFinite(n) || n <= 0) return null;
  if (!isPidAlive(n)) return null;
  return n;
}
async function getNpmPrefix() {
  return new Promise((resolve) => {
    const child = spawn("npm", ["config", "get", "prefix"], {
      stdio: ["ignore", "pipe", "ignore"]
    });
    let buf = "";
    child.stdout.on("data", (chunk) => {
      buf += chunk.toString("utf8");
    });
    child.on("error", () => {
      resolve(null);
    });
    child.on("exit", (code) => {
      if (code !== 0) {
        resolve(null);
        return;
      }
      const trimmed = buf.trim();
      resolve(trimmed.length > 0 ? trimmed : null);
    });
  });
}
async function fail(cfg, message) {
  await advanceUpgradeStage(cfg.home, "failed", { error: message });
}
function sleep(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}
main().catch(async (err) => {
  const home = process.env["TWENTYTWOHUNDRED_HOME"];
  if (home === void 0 || home.length === 0) {
    process.stderr.write(
      `upgrade-runner fatal: ${err instanceof Error ? err.message : String(err)}
`
    );
    process.exit(1);
  }
  try {
    const current = await readUpgradeStatus(home);
    const now = (/* @__PURE__ */ new Date()).toISOString();
    if (current) {
      await writeUpgradeStatus(home, {
        ...current,
        stage: "failed",
        updated_at: now,
        finished_at: now,
        error: err instanceof Error ? err.message : String(err)
      });
    }
  } catch {
  }
  process.exit(1);
});
//# sourceMappingURL=upgrade-runner.js.map
//# sourceMappingURL=upgrade-runner.js.map