296 lines
8.9 KiB
JavaScript
296 lines
8.9 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";
|
|
|
|
// GitLink release download base URL
|
|
// Format: https://www.gitlink.org.cn/Gitlink/gitlink-cli/releases
|
|
// Attachment download: https://www.gitlink.org.cn/api/attachments/{attachment_id}
|
|
const RELEASE_BASE = "https://www.gitlink.org.cn";
|
|
const REPO_OWNER = "Gitlink";
|
|
const REPO_NAME = "gitlink-cli";
|
|
|
|
function getPlatformInfo(platform = os.platform(), 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}. ` +
|
|
`Supported: darwin-x64, darwin-arm64, linux-x64, linux-arm64, win32-x64, win32-arm64`
|
|
);
|
|
}
|
|
|
|
return { platform: goPlatform, arch: goArch, isWindows: platform === "win32" };
|
|
}
|
|
|
|
function getBinaryName(platform) {
|
|
return platform === "windows" ? BINARY_NAME + ".exe" : BINARY_NAME;
|
|
}
|
|
|
|
function getArchiveName(platform, arch) {
|
|
const ext = platform === "windows" ? ".zip" : ".tar.gz";
|
|
return `${BINARY_NAME}_${VERSION}_${platform}_${arch}${ext}`;
|
|
}
|
|
|
|
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) => {
|
|
// Follow redirects
|
|
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 (max ${maxRedirects})`));
|
|
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} when downloading ${currentUrl}`)
|
|
);
|
|
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(`Failed to parse JSON: ${e.message}`));
|
|
}
|
|
});
|
|
} 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("Request timed out"));
|
|
});
|
|
}
|
|
|
|
doRequest(url);
|
|
});
|
|
}
|
|
|
|
async function findReleaseAsset(platform, arch) {
|
|
const archiveName = getArchiveName(platform, arch);
|
|
const tagName = `v${VERSION}`;
|
|
|
|
// Try fetching release info from GitLink API
|
|
const apiUrl = `${RELEASE_BASE}/api/${REPO_OWNER}/${REPO_NAME}/releases.json`;
|
|
console.log(`Fetching release info from ${apiUrl}`);
|
|
|
|
try {
|
|
const releases = await fetch(apiUrl, { json: true });
|
|
|
|
// Find the release matching our version
|
|
let release = null;
|
|
|
|
if (Array.isArray(releases)) {
|
|
release = releases.find(
|
|
(r) => r.tag_name === tagName || r.tag_name === VERSION
|
|
);
|
|
if (!release && releases.length > 0) {
|
|
// Fall back to latest release
|
|
release = releases[0];
|
|
}
|
|
} else if (releases && releases.releases) {
|
|
const list = releases.releases;
|
|
release = list.find(
|
|
(r) => r.tag_name === tagName || r.tag_name === VERSION
|
|
);
|
|
if (!release && list.length > 0) {
|
|
release = list[0];
|
|
}
|
|
}
|
|
|
|
if (release && release.attachments) {
|
|
// Try exact match first
|
|
let asset = release.attachments.find(
|
|
(a) => a.title === archiveName || a.filename === archiveName
|
|
);
|
|
// If no exact match (version mismatch on fallback release), match by platform/arch pattern
|
|
if (!asset) {
|
|
const ext = platform === "windows" ? ".zip" : ".tar.gz";
|
|
const pattern = `_${platform}_${arch}${ext}`;
|
|
asset = release.attachments.find(
|
|
(a) => (a.title || a.filename || "").endsWith(pattern)
|
|
);
|
|
}
|
|
if (asset) {
|
|
// Return the download URL for this attachment
|
|
let downloadUrl = asset.url || `${RELEASE_BASE}/api/attachments/${asset.id}`;
|
|
// Ensure absolute URL
|
|
if (downloadUrl.startsWith("/")) {
|
|
downloadUrl = RELEASE_BASE + downloadUrl;
|
|
}
|
|
return downloadUrl;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.log(`Warning: Could not fetch release info: ${e.message}`);
|
|
}
|
|
|
|
// Fallback: try direct download URL pattern
|
|
return `${RELEASE_BASE}/api/${REPO_OWNER}/${REPO_NAME}/releases/${tagName}/assets/${archiveName}`;
|
|
}
|
|
|
|
async function downloadAndExtract(url, destDir, platform) {
|
|
console.log(`Downloading ${BINARY_NAME} from ${url}...`);
|
|
|
|
const data = await fetch(url);
|
|
const isWindows = platform === "windows";
|
|
const archiveExt = isWindows ? "download.zip" : "download.tar.gz";
|
|
const archivePath = path.join(destDir, archiveExt);
|
|
|
|
fs.writeFileSync(archivePath, data);
|
|
console.log(`Downloaded ${(data.length / 1024 / 1024).toFixed(1)} MB`);
|
|
|
|
// Extract
|
|
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);
|
|
|
|
// Find the binary in extracted files
|
|
const binaryName = getBinaryName(platform);
|
|
const binaryPath = path.join(destDir, binaryName);
|
|
|
|
if (!fs.existsSync(binaryPath)) {
|
|
// It might be in a subdirectory
|
|
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`);
|
|
}
|
|
|
|
// Make executable (not needed on Windows)
|
|
if (!isWindows) {
|
|
fs.chmodSync(binaryPath, 0o755);
|
|
}
|
|
console.log(`Installed ${BINARY_NAME} to ${binaryPath}`);
|
|
}
|
|
|
|
async function main() {
|
|
let platformInfo = null;
|
|
let archiveName = null;
|
|
|
|
try {
|
|
platformInfo = getPlatformInfo();
|
|
const { platform, arch } = platformInfo;
|
|
archiveName = getArchiveName(platform, arch);
|
|
console.log(`Platform: ${platform}-${arch}`);
|
|
console.log(`Expected release asset: ${archiveName}`);
|
|
|
|
const binDir = path.join(__dirname, "..", "bin");
|
|
if (!fs.existsSync(binDir)) {
|
|
fs.mkdirSync(binDir, { recursive: true });
|
|
}
|
|
|
|
const binaryPath = path.join(binDir, getBinaryName(platform));
|
|
|
|
// If binary already exists, check version matches
|
|
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, skipping download.`);
|
|
return;
|
|
}
|
|
console.log(`${BINARY_NAME} version mismatch (got: ${output.trim()}, want: ${VERSION}), updating...`);
|
|
} catch (e) {
|
|
console.log(`${BINARY_NAME} binary exists but is not compatible, re-downloading...`);
|
|
}
|
|
fs.unlinkSync(binaryPath);
|
|
}
|
|
|
|
const downloadUrl = await findReleaseAsset(platform, arch);
|
|
await downloadAndExtract(downloadUrl, binDir, platform);
|
|
} catch (err) {
|
|
console.error(`\nFailed to install ${BINARY_NAME}: ${err.message}`);
|
|
if (platformInfo) {
|
|
console.error(`Platform: ${platformInfo.platform}/${platformInfo.arch}`);
|
|
}
|
|
if (archiveName) {
|
|
console.error(`Expected release asset: ${archiveName}`);
|
|
}
|
|
console.error(
|
|
`\nYou 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 your PATH\n` +
|
|
` 3. Or build from source: git clone && make build\n`
|
|
);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
if (require.main === module) {
|
|
main();
|
|
}
|
|
|
|
module.exports = {
|
|
getPlatformInfo,
|
|
getBinaryName,
|
|
getArchiveName,
|
|
findReleaseAsset,
|
|
};
|