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 });