#!/bin/sh
# Download, inspect, verify published SHA-256, then run this saved file. Never pipe a download into a shell.
set -eu
exec node --input-type=module - "$@" <<'WOODWIDE_INSTALLER_7303bba0a8a138f9a684f5c2'
// scripts/release-install.mjs
import fs2 from "node:fs";
import os from "node:os";
import path2 from "node:path";
import { execFileSync } from "node:child_process";
import { randomBytes as randomBytes2 } from "node:crypto";

// scripts/release-lib.mjs
import fs from "node:fs";
import path from "node:path";
import { createHash, randomBytes } from "node:crypto";
import { gzipSync, gunzipSync } from "node:zlib";
var LIMIT = 128 * 1024 * 1024;
var sha256 = (data) => createHash("sha256").update(data).digest("hex");
function checkNode(version = process.versions.node) {
  const [major, minor] = version.split(".").map(Number);
  if (!(major > 22 || major === 22 && minor >= 5)) throw new Error("Node.js >=22.5 is required.");
}
function safeName(name) {
  if (typeof name !== "string" || !/^[A-Za-z0-9_.\/-]+$/.test(name) || name.startsWith("/") || name.length > 90 || name.split("/").some((p) => !p || p === "." || p === "..")) throw new Error("Unsafe archive path");
  return name;
}
function safeVersion(v) {
  if (typeof v !== "string" || !/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[A-Za-z0-9]+(?:[.-][A-Za-z0-9]+)*)?$/.test(v) || v.length > 64) throw new Error("Invalid release version");
  return v;
}
function noSymlinks(file) {
  const abs = path.resolve(file);
  let at = path.parse(abs).root;
  for (const part of abs.slice(at.length).split(path.sep).filter(Boolean)) {
    at = path.join(at, part);
    try {
      if (fs.lstatSync(at).isSymbolicLink()) throw new Error("Refusing symlink path component");
    } catch (e) {
      if (e.code !== "ENOENT") throw e;
    }
  }
  return abs;
}
function privateWrite(file, data, mode = 384) {
  noSymlinks(file);
  fs.mkdirSync(path.dirname(file), { recursive: true, mode: 448 });
  const tmp = `${file}.${randomBytes(8).toString("hex")}.tmp`;
  try {
    const fd = fs.openSync(tmp, "wx", mode);
    try {
      fs.writeFileSync(fd, data);
      fs.fchmodSync(fd, mode);
      fs.fsyncSync(fd);
    } finally {
      fs.closeSync(fd);
    }
    fs.renameSync(tmp, file);
  } finally {
    fs.rmSync(tmp, { force: true });
  }
}
var shQuote = (value) => `'${String(value).replaceAll("'", "'\\''")}'`;
function readArchive(packed) {
  if (packed.length > LIMIT / 2) throw new Error("Compressed release too large");
  const bytes = gunzipSync(packed, { maxOutputLength: LIMIT });
  const out = /* @__PURE__ */ new Map();
  let offset = 0;
  let ended = false;
  const str = (b, a, n) => b.subarray(a, a + n).toString("ascii").split("\0")[0];
  const num = (b, a, n) => {
    const s = str(b, a, n).trim();
    if (!/^[0-7]+$/.test(s)) throw new Error("Invalid tar numeric field");
    return parseInt(s, 8);
  };
  while (offset + 512 <= bytes.length) {
    const b = bytes.subarray(offset, offset + 512);
    offset += 512;
    if (b.every((x) => x === 0)) {
      if (bytes.length - offset < 512 || !bytes.subarray(offset).every((x) => x === 0)) throw new Error("Invalid tar terminator");
      ended = true;
      break;
    }
    const stored = num(b, 148, 8);
    const copy = Buffer.from(b);
    copy.fill(32, 148, 156);
    if (copy.reduce((a, n) => a + n, 0) !== stored) throw new Error("Invalid tar checksum");
    if (str(b, 257, 6) !== "ustar" || ![0, 48].includes(b[156]) || str(b, 157, 100) || str(b, 345, 155)) throw new Error("Only regular USTAR files are supported");
    const full = str(b, 0, 100);
    if (!full.startsWith("woodwide/")) throw new Error("Invalid archive root");
    const name = safeName(full.slice(9));
    const size = num(b, 124, 12);
    const mode = num(b, 100, 8);
    if (![420, 493].includes(mode) || size > 32 * 1024 * 1024 || offset + size > bytes.length || out.has(name) || out.size >= 2048) throw new Error("Invalid tar entry");
    out.set(name, { data: bytes.subarray(offset, offset + size), mode });
    offset += Math.ceil(size / 512) * 512;
  }
  if (!ended || !out.size) throw new Error("Incomplete archive");
  return out;
}
function verifyArchive(bytes, expected) {
  if (!/^[a-f0-9]{64}$/.test(expected ?? "") || sha256(bytes) !== expected) throw new Error("SHA-256 mismatch or missing trusted digest");
  const files = readArchive(bytes);
  const manifest = JSON.parse(files.get("release.json")?.data.toString("utf8") ?? "null");
  if (manifest?.schema !== 1 || manifest.product !== "woodwide" || !manifest.files || Array.isArray(manifest.files)) throw new Error("Invalid release manifest");
  safeVersion(manifest.version);
  if (!manifest.files["cli/woodwide.mjs"] || !manifest.files.LICENSE || !manifest.files["THIRD-PARTY-NOTICES.txt"]) throw new Error("Missing required distribution files");
  if (files.size !== Object.keys(manifest.files).length + 1) throw new Error("Unlisted archive entries");
  for (const [name, f] of Object.entries(manifest.files)) {
    safeName(name);
    const actual = files.get(name);
    if (!actual || f.size !== actual.data.length || f.mode !== actual.mode || sha256(actual.data) !== f.sha256) throw new Error("Release inventory mismatch");
  }
  return { manifest, files };
}
function args(argv, booleans = []) {
  const result = { _: [] };
  for (let i = 0; i < argv.length; i++) {
    const a = argv[i];
    if (!a.startsWith("--")) {
      result._.push(a);
      continue;
    }
    const key = a.slice(2);
    if (key in result) throw new Error("Duplicate option");
    if (booleans.includes(key)) result[key] = true;
    else {
      if (!argv[i + 1] || argv[i + 1].startsWith("--")) throw new Error(`Missing value for --${key}`);
      result[key] = argv[++i];
    }
  }
  return result;
}

// scripts/release-install.mjs
function main() {
  checkNode();
  const a = args(process.argv.slice(2), ["dry-run", "hooks-removed"]);
  if (a._[0] === "help" || !a._[0]) {
    console.log("install|upgrade --archive FILE --sha256 DIGEST [--prefix DIR] [--bin-dir DIR] [--dry-run]\nuninstall --hooks-removed [--prefix DIR] [--bin-dir DIR]");
    return;
  }
  for (const key of Object.keys(a)) if (!["_", "archive", "sha256", "prefix", "bin-dir", "dry-run", "hooks-removed"].includes(key)) throw new Error("Unknown installer option");
  if (a._.length !== 1 || !["install", "upgrade", "uninstall"].includes(a._[0])) throw new Error("Unknown installer action");
  if (!["linux", "darwin"].includes(process.platform)) throw new Error("This installer supports Linux and macOS; Windows is not validated.");
  const prefix = noSymlinks(a.prefix ?? path2.join(os.homedir(), ".local/share/woodwide"));
  const binDir = noSymlinks(a["bin-dir"] ?? path2.join(os.homedir(), ".local/bin"));
  const dataHome = path2.resolve(process.env.WOODWIDE_HOME ?? path2.join(os.homedir(), ".woodwide"));
  if (prefix === path2.parse(prefix).root || prefix === os.homedir() || prefix === dataHome || dataHome.startsWith(prefix + path2.sep) || prefix.startsWith(dataHome + path2.sep)) throw new Error("Code prefix must be separate from user data and home roots");
  const receiptPath = path2.join(prefix, "install-receipt.json");
  noSymlinks(receiptPath);
  const receipt = fs2.existsSync(receiptPath) ? JSON.parse(fs2.readFileSync(receiptPath, "utf8")) : { schema: 1, versions: {} };
  if (receipt.schema !== 1 || !receipt.versions || receipt.binDir && receipt.binDir !== binDir) throw new Error("Invalid receipt or mismatched bin directory");
  const launcher = path2.join(binDir, "woodwide");
  noSymlinks(launcher);
  if (fs2.existsSync(launcher) && (!receipt.launcherHash || sha256(fs2.readFileSync(launcher)) !== receipt.launcherHash)) throw new Error("Refusing to overwrite an unowned or modified launcher");
  const current = path2.join(prefix, "current");
  if (fs2.existsSync(current) || (() => {
    try {
      return fs2.lstatSync(current).isSymbolicLink();
    } catch {
      return false;
    }
  })()) {
    if (!fs2.lstatSync(current).isSymbolicLink() || fs2.readlinkSync(current) !== `versions/${receipt.current}`) throw new Error("Refusing unowned current pointer");
  }
  const action = a._[0];
  if (action === "uninstall") {
    if (!a["hooks-removed"]) throw new Error("Remove Woodwide hooks/plugins first, then acknowledge with --hooks-removed. Local memory is retained.");
    if (a["dry-run"]) {
      console.log("Would remove owned launcher and code; local memory and all harness configuration stay untouched.");
      return;
    }
    if (!fs2.existsSync(receiptPath)) throw new Error("No managed installation");
  }
  let verified;
  let packed;
  if (action !== "uninstall") {
    noSymlinks(a.archive ?? "");
    if (!a.archive || fs2.statSync(a.archive).size > 64 * 1024 * 1024) throw new Error("Missing or oversized archive");
    packed = fs2.readFileSync(a.archive);
    verified = verifyArchive(packed, a.sha256);
    if (a["dry-run"]) {
      console.log(`Verified ${verified.manifest.version}; would install ${verified.files.size} files. No hooks or user config will change.`);
      return;
    }
  }
  fs2.mkdirSync(prefix, { recursive: true, mode: 448 });
  const lock = path2.join(prefix, ".install-lock");
  noSymlinks(lock);
  fs2.mkdirSync(lock, { mode: 448 });
  let stage;
  try {
    const fresh = fs2.existsSync(receiptPath) ? JSON.parse(fs2.readFileSync(receiptPath, "utf8")) : { schema: 1, versions: {} };
    if (JSON.stringify(fresh) !== JSON.stringify(receipt)) throw new Error("Installation changed; rerun");
    const checkLauncher = () => {
      noSymlinks(launcher);
      if (fs2.existsSync(launcher) && (!receipt.launcherHash || sha256(fs2.readFileSync(launcher)) !== receipt.launcherHash)) throw new Error("Launcher changed; refusing overwrite");
    };
    checkLauncher();
    if (action === "uninstall") {
      for (const [v, info] of Object.entries(receipt.versions)) {
        const dir = noSymlinks(path2.join(prefix, "versions", v));
        if (path2.dirname(dir) !== path2.join(prefix, "versions")) throw new Error("Invalid receipt version");
        verifyTree(dir, info.files);
      }
      if (fs2.existsSync(launcher)) fs2.unlinkSync(launcher);
      if (fs2.existsSync(current)) fs2.unlinkSync(current);
      for (const v of Object.keys(receipt.versions)) fs2.rmSync(path2.join(prefix, "versions", v), { recursive: true });
      fs2.unlinkSync(receiptPath);
      console.log("Owned code removed. Local memory, exports, accounts, uploads, and harness settings were NOT deleted.");
      return;
    }
    const { manifest, files } = verified;
    let sqliteArgs = [];
    try {
      execFileSync(process.execPath, ["-e", "require('node:sqlite')"], { stdio: "ignore" });
    } catch {
      sqliteArgs = ["--experimental-sqlite"];
      execFileSync(process.execPath, [...sqliteArgs, "-e", "require('node:sqlite')"], { stdio: "ignore" });
    }
    const versions = noSymlinks(path2.join(prefix, "versions"));
    fs2.mkdirSync(versions, { recursive: true, mode: 448 });
    const destination = noSymlinks(path2.join(versions, manifest.version));
    const inventory = Object.fromEntries([...files].map(([n, f]) => [n, { sha256: sha256(f.data), mode: f.mode }]));
    if (fs2.existsSync(destination)) {
      if (receipt.versions[manifest.version]?.archiveSha256 !== a.sha256) throw new Error("Version already exists with different bytes; publish a new version");
      verifyTree(destination, inventory);
    } else {
      stage = fs2.mkdtempSync(path2.join(versions, ".stage-"));
      for (const [name, f] of files) {
        const dest = path2.join(stage, name);
        fs2.mkdirSync(path2.dirname(dest), { recursive: true, mode: 448 });
        fs2.writeFileSync(dest, f.data, { mode: f.mode, flag: "wx" });
        fs2.chmodSync(dest, f.mode);
      }
      const version = execFileSync(process.execPath, [...sqliteArgs, path2.join(stage, "cli/woodwide.mjs"), "version"], { encoding: "utf8", timeout: 15e3, env: { PATH: process.env.PATH, HOME: os.homedir(), NO_COLOR: "1" } }).trim();
      if (version !== manifest.version) throw new Error("CLI version disagrees with manifest");
      fs2.renameSync(stage, destination);
      stage = void 0;
    }
    fs2.mkdirSync(binDir, { recursive: true, mode: 493 });
    const wrapper = `#!/bin/sh
# Woodwide managed launcher; hooks remain version-pinned until explicitly reinstalled.
exec ${[process.execPath, ...sqliteArgs, path2.join(destination, "cli/woodwide.mjs")].map(shQuote).join(" ")} "$@"
`;
    checkLauncher();
    privateWrite(launcher, wrapper, 493);
    const next = path2.join(prefix, `.current-${randomBytes2(6).toString("hex")}`);
    fs2.symlinkSync(`versions/${manifest.version}`, next);
    fs2.renameSync(next, current);
    receipt.current = manifest.version;
    receipt.binDir = binDir;
    receipt.launcherHash = sha256(wrapper);
    receipt.versions[manifest.version] = { archiveSha256: a.sha256, files: inventory };
    privateWrite(receiptPath, JSON.stringify(receipt, null, 2) + "\n");
    console.log(`Installed Woodwide ${manifest.version}. Launcher: ${launcher}
No shell profile, account, hooks, trust hashes, or user configuration changed.
Initialize privately with: ${shQuote(launcher)} init --local --no-install
Hooks are a separate reviewed action. Existing hooks still target their previous version.`);
  } finally {
    if (stage) fs2.rmSync(stage, { recursive: true, force: true });
    fs2.rmdirSync(lock);
  }
}
function verifyTree(dir, inventory) {
  if (!inventory || typeof inventory !== "object") throw new Error("Invalid file inventory");
  const found = [];
  function walk(p) {
    for (const e of fs2.readdirSync(p, { withFileTypes: true })) {
      const f = path2.join(p, e.name);
      if (e.isSymbolicLink()) throw new Error("Modified code tree contains a symlink");
      if (e.isDirectory()) {
        const name = path2.relative(dir, f).split(path2.sep).join("/") + "/";
        if (!Object.keys(inventory).some((n) => n.startsWith(name))) throw new Error("Unknown directory in code tree; refusing removal");
        walk(f);
      } else if (e.isFile()) found.push(path2.relative(dir, f).split(path2.sep).join("/"));
      else throw new Error("Unexpected code tree entry");
    }
  }
  walk(dir);
  if (found.length !== Object.keys(inventory).length) throw new Error("Modified code tree; retain and inspect it manually");
  for (const n of found) if (!inventory[n] || sha256(fs2.readFileSync(path2.join(dir, n))) !== inventory[n].sha256 || (fs2.statSync(path2.join(dir, n)).mode & 511) !== inventory[n].mode) throw new Error("Modified code file; refusing removal or replacement");
}
try {
  main();
} catch (e) {
  console.error(`Woodwide installer: ${e.message}`);
  process.exitCode = 1;
}

WOODWIDE_INSTALLER_7303bba0a8a138f9a684f5c2
