forked from Gitlink/gitlink-cli
189 lines
6.7 KiB
JavaScript
189 lines
6.7 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
"use strict";
|
|
|
|
const os = require("os");
|
|
const path = require("path");
|
|
const fs = require("fs");
|
|
const https = require("https");
|
|
const http = require("http");
|
|
const { execSync } = require("child_process");
|
|
|
|
const PACKAGE = require("../package.json");
|
|
const VERSION = PACKAGE.version;
|
|
const BINARY_NAME = "gitlink-cli";
|
|
|
|
const RELEASE_BASE = "https://www.gitlink.org.cn";
|
|
const REPO_OWNER = "Gitlink";
|
|
const REPO_NAME = "gitlink-cli";
|
|
|
|
const GITHUB_RELEASE_BASE = "https://github.com";
|
|
const GITHUB_REPO_OWNER = "ccfos";
|
|
const GITHUB_REPO_NAME = "gitlink-cli";
|
|
|
|
function getPlatformInfo() {
|
|
const platform = os.platform();
|
|
const arch = os.arch();
|
|
const platformMap = { darwin: "darwin", linux: "linux", win32: "windows" };
|
|
const archMap = { x64: "amd64", arm64: "arm64" };
|
|
const goPlatform = platformMap[platform];
|
|
const goArch = archMap[arch];
|
|
if (!goPlatform || !goArch) {
|
|
throw new Error(`Unsupported platform: ${platform}-${arch}`);
|
|
}
|
|
return { platform: goPlatform, arch: goArch, isWindows: platform === "win32" };
|
|
}
|
|
|
|
function getBinaryName(isWindows) {
|
|
return isWindows ? BINARY_NAME + ".exe" : BINARY_NAME;
|
|
}
|
|
|
|
function getArchiveExt(isWindows) {
|
|
return isWindows ? ".zip" : ".tar.gz";
|
|
}
|
|
|
|
function getArchiveName(platform, arch, isWindows) {
|
|
return `${BINARY_NAME}_${VERSION}_${platform}_${arch}${getArchiveExt(isWindows)}`;
|
|
}
|
|
|
|
function fetch(url, options = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
const maxRedirects = options.maxRedirects || 5;
|
|
let redirectCount = 0;
|
|
function doRequest(currentUrl) {
|
|
const mod = currentUrl.startsWith("https") ? https : http;
|
|
const req = mod.get(currentUrl, (res) => {
|
|
if ((res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307 || res.statusCode === 308) && res.headers.location) {
|
|
redirectCount++;
|
|
if (redirectCount > maxRedirects) { reject(new Error("Too many redirects")); return; }
|
|
let redirectUrl = res.headers.location;
|
|
if (redirectUrl.startsWith("/")) {
|
|
const parsed = new URL(currentUrl);
|
|
redirectUrl = `${parsed.protocol}//${parsed.host}${redirectUrl}`;
|
|
}
|
|
doRequest(redirectUrl);
|
|
return;
|
|
}
|
|
if (res.statusCode !== 200) {
|
|
reject(new Error(`HTTP ${res.statusCode}`));
|
|
return;
|
|
}
|
|
if (options.json) {
|
|
let body = "";
|
|
res.on("data", (chunk) => (body += chunk));
|
|
res.on("end", () => {
|
|
try { resolve(JSON.parse(body)); } catch (e) { reject(new Error("JSON parse failed")); }
|
|
});
|
|
} else {
|
|
const chunks = [];
|
|
res.on("data", (chunk) => chunks.push(chunk));
|
|
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
}
|
|
});
|
|
req.on("error", reject);
|
|
req.setTimeout(60000, () => { req.destroy(); reject(new Error("Timeout")); });
|
|
}
|
|
doRequest(url);
|
|
});
|
|
}
|
|
|
|
async function findReleaseAsset(platform, arch, isWindows) {
|
|
const archiveName = getArchiveName(platform, arch, isWindows);
|
|
const tagName = `v${VERSION}`;
|
|
const apiUrl = `${RELEASE_BASE}/api/${REPO_OWNER}/${REPO_NAME}/releases.json`;
|
|
|
|
try {
|
|
const releases = await fetch(apiUrl, { json: true });
|
|
const rlist = Array.isArray(releases) ? releases : (releases && releases.releases ? releases.releases : []);
|
|
let release = rlist.find(r => r.tag_name === tagName || r.tag_name === VERSION);
|
|
if (!release && rlist.length > 0) release = rlist[0];
|
|
|
|
if (release && release.attachments) {
|
|
let asset = release.attachments.find(a => a.title === archiveName || a.filename === archiveName);
|
|
if (!asset) {
|
|
const pattern = `_${platform}_${arch}${getArchiveExt(isWindows)}`;
|
|
asset = release.attachments.find(a => (a.title || a.filename || "").endsWith(pattern));
|
|
}
|
|
if (asset) {
|
|
let url = asset.url || `${RELEASE_BASE}/api/attachments/${asset.id}`;
|
|
if (url.startsWith("/")) url = RELEASE_BASE + url;
|
|
return url;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.log(`Warning: GitLink API failed: ${e.message}, trying GitHub...`);
|
|
}
|
|
|
|
return `${GITHUB_RELEASE_BASE}/${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}/releases/download/${tagName}/${archiveName}`;
|
|
}
|
|
|
|
async function downloadAndExtract(url, destDir, isWindows) {
|
|
console.log(`Downloading ${BINARY_NAME} from ${url}...`);
|
|
|
|
const data = await fetch(url);
|
|
console.log(`Downloaded ${(data.length / 1024 / 1024).toFixed(1)} MB`);
|
|
|
|
const archivePath = path.join(destDir, "download" + getArchiveExt(isWindows));
|
|
fs.writeFileSync(archivePath, data);
|
|
|
|
if (isWindows) {
|
|
execSync(
|
|
`powershell -NoProfile -Command "Expand-Archive -Force -Path '${archivePath}' -DestinationPath '${destDir}'"`,
|
|
{ stdio: "pipe" }
|
|
);
|
|
} else {
|
|
execSync(`tar -xzf "${archivePath}" -C "${destDir}"`, { stdio: "pipe" });
|
|
}
|
|
fs.unlinkSync(archivePath);
|
|
|
|
const binaryName = getBinaryName(isWindows);
|
|
const binaryPath = path.join(destDir, binaryName);
|
|
if (!fs.existsSync(binaryPath)) {
|
|
const files = fs.readdirSync(destDir);
|
|
for (const file of files) {
|
|
const subPath = path.join(destDir, file, binaryName);
|
|
if (fs.existsSync(subPath)) { fs.renameSync(subPath, binaryPath); break; }
|
|
}
|
|
}
|
|
if (!fs.existsSync(binaryPath)) throw new Error(`Binary "${binaryName}" not found after extraction`);
|
|
if (!isWindows) fs.chmodSync(binaryPath, 0o755);
|
|
}
|
|
|
|
async function main() {
|
|
const { platform, arch, isWindows } = getPlatformInfo();
|
|
console.log(`Platform: ${platform}-${arch}`);
|
|
|
|
const binDir = path.join(__dirname, "..", "bin");
|
|
if (!fs.existsSync(binDir)) { fs.mkdirSync(binDir, { recursive: true }); }
|
|
|
|
const binaryPath = path.join(binDir, getBinaryName(isWindows));
|
|
if (fs.existsSync(binaryPath)) {
|
|
try {
|
|
const output = execSync(`"${binaryPath}" version`, { encoding: "utf-8", stdio: "pipe", timeout: 5000 });
|
|
if (output.includes(VERSION)) {
|
|
console.log(`${BINARY_NAME} v${VERSION} already installed.`);
|
|
return;
|
|
}
|
|
console.log(`Version mismatch (got: ${output.trim()}, want: ${VERSION}), updating...`);
|
|
} catch (e) {}
|
|
fs.unlinkSync(binaryPath);
|
|
}
|
|
|
|
try {
|
|
const downloadUrl = await findReleaseAsset(platform, arch, isWindows);
|
|
await downloadAndExtract(downloadUrl, binDir, isWindows);
|
|
console.log(`${BINARY_NAME} v${VERSION} installed successfully.`);
|
|
} catch (err) {
|
|
console.error(`\n Failed to install ${BINARY_NAME}: ${err.message}`);
|
|
console.error(
|
|
`\n You can install manually:\n` +
|
|
` 1. Download from https://www.gitlink.org.cn/${REPO_OWNER}/${REPO_NAME}/releases\n` +
|
|
` 2. Extract and place the binary in: ${binDir}\n` +
|
|
` 3. Or retry: npm run postinstall\n`
|
|
);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
main();
|