/ npm / install.js
install.js
  1  const { execFileSync } = require("child_process");
  2  const fs = require("fs");
  3  const path = require("path");
  4  const https = require("https");
  5  const crypto = require("crypto");
  6  
  7  const REPO = "Kocoro-lab/ShanClaw";
  8  const BIN_DIR = path.join(__dirname, "bin");
  9  
 10  function getPlatform() {
 11    const p = process.platform;
 12    if (p !== "darwin" && p !== "linux") {
 13      throw new Error("Unsupported platform: " + p + ". shan supports macOS and Linux.");
 14    }
 15    return p;
 16  }
 17  
 18  function getArch() {
 19    const map = { x64: "amd64", arm64: "arm64" };
 20    const a = map[process.arch];
 21    if (!a) {
 22      throw new Error("Unsupported architecture: " + process.arch);
 23    }
 24    return a;
 25  }
 26  
 27  function fetch(url) {
 28    return new Promise((resolve, reject) => {
 29      https.get(url, { headers: { "User-Agent": "shan-cli-npm" } }, (res) => {
 30        if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
 31          return fetch(res.headers.location).then(resolve, reject);
 32        }
 33        if (res.statusCode !== 200) {
 34          return reject(new Error("HTTP " + res.statusCode + " for " + url));
 35        }
 36        const chunks = [];
 37        res.on("data", (c) => chunks.push(c));
 38        res.on("end", () => resolve(Buffer.concat(chunks)));
 39        res.on("error", reject);
 40      }).on("error", reject);
 41    });
 42  }
 43  
 44  async function main() {
 45    const platform = getPlatform();
 46    const arch = getArch();
 47    console.log("shan: detecting platform " + platform + "/" + arch);
 48  
 49    // Get latest release
 50    const releaseData = await fetch(
 51      "https://api.github.com/repos/" + REPO + "/releases/latest"
 52    );
 53    const release = JSON.parse(releaseData.toString());
 54    const version = release.tag_name.replace(/^v/, "");
 55    console.log("shan: installing v" + version + "...");
 56  
 57    const filename = "shan_" + version + "_" + platform + "_" + arch + ".tar.gz";
 58    const asset = release.assets.find((a) => a.name === filename);
 59    if (!asset) {
 60      throw new Error("No release asset found for " + filename);
 61    }
 62  
 63    // Download binary
 64    const tarball = await fetch(asset.browser_download_url);
 65  
 66    // Verify checksum
 67    const checksumAsset = release.assets.find((a) => a.name === "checksums.txt");
 68    if (checksumAsset) {
 69      const checksumData = await fetch(checksumAsset.browser_download_url);
 70      const line = checksumData.toString().split("\n").find((l) => l.includes(filename));
 71      if (line) {
 72        const expected = line.split(/\s+/)[0];
 73        const actual = crypto.createHash("sha256").update(tarball).digest("hex");
 74        if (actual !== expected) {
 75          throw new Error("Checksum mismatch for " + filename);
 76        }
 77        console.log("shan: checksum verified");
 78      }
 79    }
 80  
 81    // Extract with system tar
 82    fs.mkdirSync(BIN_DIR, { recursive: true });
 83    const tmpTar = path.join(BIN_DIR, "_shan.tar.gz");
 84    fs.writeFileSync(tmpTar, tarball);
 85    try {
 86      execFileSync("tar", ["-xzf", "_shan.tar.gz", "shan"], { cwd: BIN_DIR });
 87      // ax_server is only present in darwin archives — not an error on linux
 88      try {
 89        execFileSync("tar", ["-xzf", "_shan.tar.gz", "ax_server"], { cwd: BIN_DIR });
 90        fs.chmodSync(path.join(BIN_DIR, "ax_server"), 0o755);
 91      } catch (_) {}
 92    } finally {
 93      fs.unlinkSync(tmpTar);
 94    }
 95  
 96    fs.chmodSync(path.join(BIN_DIR, "shan"), 0o755);
 97    console.log("shan: v" + version + " installed successfully");
 98  }
 99  
100  main().catch((err) => {
101    console.error("shan install failed: " + err.message);
102    process.exit(1);
103  });