gitlink-cli/npm/scripts/install.js

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