26382a7ac6
CI / Clippy (push) Failing after 15m13s
CI / Test (ubuntu-latest) (push) Failing after 16m1s
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Build (no embeddings / no ORT) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Cookbook (Node) (push) Has been cancelled
CI / Pi Extension (Node) (push) Has been cancelled
CI / Rust SDK (lean-ctx-client) (push) Has been cancelled
CI / Embed SDK (lean-ctx-sdk) (push) Has been cancelled
CI / Python SDK (leanctx) (push) Has been cancelled
CI / Hermes Plugin (Python) (push) Has been cancelled
CI / SDK Conformance Matrix (push) Has been cancelled
CI / Coverage (push) Has been cancelled
CI / cargo-deny (push) Has been cancelled
CI / Adversarial Safety (push) Has been cancelled
CI / Benchmarks (push) Has been cancelled
CI / Output-Quality Gate (eval A/B) (push) Has been cancelled
CI / Documentation (push) Has been cancelled
CI / CI Green (push) Has been cancelled
JetBrains Plugin / Actionlint (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (rust) (push) Has been cancelled
JetBrains Plugin / Validation (push) Has been cancelled
JetBrains Plugin / Build (push) Has been cancelled
JetBrains Plugin / Test (push) Has been cancelled
Security Check / Security Scan (push) Has been cancelled
232 lines
8.3 KiB
JavaScript
232 lines
8.3 KiB
JavaScript
#!/usr/bin/env node
|
|
"use strict";
|
|
|
|
const { execSync } = require("child_process");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const https = require("https");
|
|
const { createGunzip } = require("zlib");
|
|
const crypto = require("crypto");
|
|
|
|
const REPO = "yvgude/lean-ctx";
|
|
const BIN_DIR = path.join(__dirname, "bin");
|
|
const IS_WIN = process.platform === "win32";
|
|
const BINARY_NAME = IS_WIN ? "lean-ctx.exe" : "lean-ctx";
|
|
const BINARY_PATH = path.join(BIN_DIR, BINARY_NAME);
|
|
|
|
function getGlibcVersion() {
|
|
try {
|
|
const out = execSync("ldd --version 2>&1 || true", { encoding: "utf8" });
|
|
const match = out.match(/(\d+)\.(\d+)\s*$/m);
|
|
if (match) return { major: parseInt(match[1]), minor: parseInt(match[2]) };
|
|
} catch {}
|
|
return null;
|
|
}
|
|
|
|
function getTarget() {
|
|
const platform = process.platform;
|
|
const arch = process.arch;
|
|
|
|
if (platform === "linux") {
|
|
let libc = "musl";
|
|
const glibc = getGlibcVersion();
|
|
if (glibc && (glibc.major > 2 || (glibc.major === 2 && glibc.minor >= 35))) {
|
|
libc = "gnu";
|
|
}
|
|
const archMap = { x64: "x86_64", arm64: "aarch64" };
|
|
const rustArch = archMap[arch];
|
|
if (!rustArch) {
|
|
console.error(`Unsupported architecture: ${arch}`);
|
|
process.exit(1);
|
|
}
|
|
return `${rustArch}-unknown-linux-${libc}`;
|
|
}
|
|
|
|
const key = `${platform}-${arch}`;
|
|
const targets = {
|
|
"darwin-x64": "x86_64-apple-darwin",
|
|
"darwin-arm64": "aarch64-apple-darwin",
|
|
"win32-x64": "x86_64-pc-windows-msvc",
|
|
};
|
|
|
|
const target = targets[key];
|
|
if (!target) {
|
|
console.error(`Unsupported platform: ${key}`);
|
|
console.error("Build from source instead: cargo install lean-ctx");
|
|
process.exit(1);
|
|
}
|
|
return target;
|
|
}
|
|
|
|
function httpsGet(url) {
|
|
return new Promise((resolve, reject) => {
|
|
const get = (u) => {
|
|
https.get(u, { headers: { "User-Agent": "lean-ctx-bin-npm" } }, (res) => {
|
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
get(res.headers.location);
|
|
return;
|
|
}
|
|
if (res.statusCode !== 200) {
|
|
reject(new Error(`HTTP ${res.statusCode} for ${u}`));
|
|
return;
|
|
}
|
|
resolve(res);
|
|
}).on("error", reject);
|
|
};
|
|
get(url);
|
|
});
|
|
}
|
|
|
|
function httpsGetJson(url) {
|
|
return new Promise((resolve, reject) => {
|
|
httpsGet(url).then((res) => {
|
|
let data = "";
|
|
res.on("data", (c) => (data += c));
|
|
res.on("end", () => {
|
|
try { resolve(JSON.parse(data)); }
|
|
catch (e) { reject(e); }
|
|
});
|
|
}).catch(reject);
|
|
});
|
|
}
|
|
|
|
async function downloadToFile(url, dest) {
|
|
const res = await httpsGet(url);
|
|
return new Promise((resolve, reject) => {
|
|
const ws = fs.createWriteStream(dest);
|
|
res.pipe(ws);
|
|
ws.on("finish", resolve);
|
|
ws.on("error", reject);
|
|
});
|
|
}
|
|
|
|
function extractTarGz(archive, destDir, binaryName) {
|
|
const gunzip = createGunzip();
|
|
const input = fs.createReadStream(archive);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const chunks = [];
|
|
input.pipe(gunzip)
|
|
.on("data", (c) => chunks.push(c))
|
|
.on("end", () => {
|
|
const buf = Buffer.concat(chunks);
|
|
let offset = 0;
|
|
while (offset < buf.length) {
|
|
const header = buf.subarray(offset, offset + 512);
|
|
if (header.every((b) => b === 0)) break;
|
|
|
|
const name = header.subarray(0, 100).toString("utf8").replace(/\0/g, "");
|
|
const sizeStr = header.subarray(124, 136).toString("utf8").replace(/\0/g, "").trim();
|
|
const size = parseInt(sizeStr, 8) || 0;
|
|
offset += 512;
|
|
|
|
const baseName = path.basename(name);
|
|
if (baseName === binaryName && size > 0) {
|
|
const dest = path.join(destDir, binaryName);
|
|
fs.writeFileSync(dest, buf.subarray(offset, offset + size));
|
|
if (!IS_WIN) fs.chmodSync(dest, 0o755);
|
|
resolve(dest);
|
|
return;
|
|
}
|
|
offset += Math.ceil(size / 512) * 512;
|
|
}
|
|
reject(new Error(`${binaryName} not found in archive`));
|
|
})
|
|
.on("error", reject);
|
|
});
|
|
}
|
|
|
|
async function main() {
|
|
if (fs.existsSync(BINARY_PATH)) {
|
|
console.log("lean-ctx binary already exists, skipping download");
|
|
return;
|
|
}
|
|
|
|
const target = getTarget();
|
|
console.log(`lean-ctx: installing for ${target}...`);
|
|
|
|
const release = await httpsGetJson(`https://api.github.com/repos/${REPO}/releases/latest`);
|
|
const tag = release.tag_name;
|
|
console.log(`lean-ctx: latest release ${tag}`);
|
|
|
|
const ext = IS_WIN ? ".zip" : ".tar.gz";
|
|
const assetName = `lean-ctx-${target}${ext}`;
|
|
const asset = (release.assets || []).find((a) => a.name === assetName);
|
|
if (!asset) {
|
|
console.error(`No binary for ${target}. Install from source: cargo install lean-ctx`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const tmpDir = fs.mkdtempSync(path.join(require("os").tmpdir(), "lean-ctx-"));
|
|
const archivePath = path.join(tmpDir, assetName);
|
|
|
|
try {
|
|
await downloadToFile(asset.browser_download_url, archivePath);
|
|
|
|
const sumsAsset = (release.assets || []).find((a) => a.name === "SHA256SUMS");
|
|
if (sumsAsset) {
|
|
const sumsText = await new Promise((resolve, reject) => {
|
|
httpsGet(sumsAsset.browser_download_url).then((res) => {
|
|
let data = "";
|
|
res.on("data", (c) => (data += c));
|
|
res.on("end", () => resolve(data));
|
|
}).catch(reject);
|
|
});
|
|
const expectedLine = sumsText.split("\n").find((l) => l.includes(assetName));
|
|
if (expectedLine) {
|
|
const expectedHash = expectedLine.trim().split(/\s+/)[0].toLowerCase();
|
|
const fileHash = crypto.createHash("sha256").update(fs.readFileSync(archivePath)).digest("hex");
|
|
if (fileHash !== expectedHash) {
|
|
throw new Error(`SHA256 mismatch: expected ${expectedHash}, got ${fileHash}. Binary may be compromised.`);
|
|
}
|
|
console.log("lean-ctx: SHA256 verified");
|
|
}
|
|
}
|
|
|
|
console.log("lean-ctx: downloaded, extracting...");
|
|
|
|
fs.mkdirSync(BIN_DIR, { recursive: true });
|
|
|
|
if (IS_WIN) {
|
|
execSync(`tar -xf "${archivePath}" -C "${BIN_DIR}"`, { stdio: "ignore" });
|
|
} else {
|
|
await extractTarGz(archivePath, BIN_DIR, "lean-ctx");
|
|
}
|
|
|
|
console.log(`lean-ctx: installed to ${BINARY_PATH}`);
|
|
|
|
// Auto-onboard unless in CI or opted out
|
|
if (!process.env.CI && process.env.LEAN_CTX_NO_ONBOARD !== "1") {
|
|
try {
|
|
const { execSync: exec } = require("child_process");
|
|
console.log("");
|
|
console.log("Running onboard (connecting your AI tools)...");
|
|
exec(`"${BINARY_PATH}" onboard`, { stdio: "inherit", timeout: 30000 });
|
|
} catch {
|
|
// Non-fatal: onboard may fail in restricted envs
|
|
}
|
|
}
|
|
|
|
console.log("");
|
|
console.log("\x1b[1m┌─────────────────────────────────────────────────────┐\x1b[0m");
|
|
console.log("\x1b[1m│\x1b[0m \x1b[32m\x1b[1m✓ lean-ctx installed successfully\x1b[0m \x1b[1m│\x1b[0m");
|
|
console.log("\x1b[1m│\x1b[0m \x1b[1m│\x1b[0m");
|
|
console.log("\x1b[1m│\x1b[0m Quick start: \x1b[1m│\x1b[0m");
|
|
console.log("\x1b[1m│\x1b[0m \x1b[1mlean-ctx wrap cursor\x1b[0m (one-command setup) \x1b[1m│\x1b[0m");
|
|
console.log("\x1b[1m│\x1b[0m \x1b[1mlean-ctx wrap claude\x1b[0m (Claude Code) \x1b[1m│\x1b[0m");
|
|
console.log("\x1b[1m│\x1b[0m \x1b[1mlean-ctx wrap codex\x1b[0m (Codex CLI) \x1b[1m│\x1b[0m");
|
|
console.log("\x1b[1m│\x1b[0m \x1b[1m│\x1b[0m");
|
|
console.log("\x1b[1m│\x1b[0m Full control: \x1b[2mlean-ctx setup\x1b[0m \x1b[1m│\x1b[0m");
|
|
console.log("\x1b[1m│\x1b[0m \x1b[2mDocs: https://leanctx.com/docs\x1b[0m \x1b[1m│\x1b[0m");
|
|
console.log("\x1b[1m└─────────────────────────────────────────────────────┘\x1b[0m");
|
|
} finally {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(`lean-ctx: installation failed: ${err.message}`);
|
|
console.error("Install from source instead: cargo install lean-ctx");
|
|
process.exit(1);
|
|
});
|