gitlink-cli/npm/scripts/update.js

430 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 colors = {
reset: "\x1b[0m",
green: "\x1b[32m",
yellow: "\x1b[33m",
red: "\x1b[31m",
cyan: "\x1b[36m",
blue: "\x1b[34m",
};
function info(msg) {
console.log(`${colors.green}[INFO]${colors.reset} ${msg}`);
}
function warn(msg) {
console.log(`${colors.yellow}[WARN]${colors.reset} ${msg}`);
}
function error(msg) {
console.log(`${colors.red}[ERROR]${colors.reset} ${msg}`);
}
function step(msg) {
console.log(`${colors.cyan}[STEP]${colors.reset} ${msg}`);
}
function debug(msg) {
if (process.env.DEBUG === "true") {
console.log(`${colors.blue}[DEBUG]${colors.reset} ${msg}`);
}
}
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(e);
}
});
} else {
res.pipe(resolve);
}
});
req.on("error", reject);
req.setTimeout(options.timeout || 30000, () => {
req.destroy();
reject(new Error(`Request timeout: ${currentUrl}`));
});
}
doRequest(url);
});
}
// 下载文件(带重试)
async function downloadFile(url, outputPath, maxAttempts = 3) {
let attempt = 1;
while (attempt <= maxAttempts) {
try {
step(`下载 (尝试 ${attempt}/${maxAttempts}): ${path.basename(url)}`);
debug(`URL: ${url}`);
await new Promise((resolve, reject) => {
const file = fs.createWriteStream(outputPath);
const mod = url.startsWith("https") ? https : http;
const req = mod.get(url, (res) => {
if (res.statusCode !== 200) {
reject(new Error(`HTTP ${res.statusCode}`));
return;
}
const totalSize = parseInt(res.headers["content-length"], 10);
let downloadedSize = 0;
res.on("data", (chunk) => {
downloadedSize += chunk.length;
if (totalSize) {
const progress = ((downloadedSize / totalSize) * 100).toFixed(1);
process.stdout.write(`\r下载进度: ${progress}%`);
}
});
res.pipe(file);
file.on("finish", () => {
file.close();
process.stdout.write("\r");
resolve();
});
file.on("error", (err) => {
fs.unlink(outputPath, () => {});
reject(err);
});
});
req.on("error", (err) => {
file.destroy();
fs.unlink(outputPath, () => {});
reject(err);
});
req.setTimeout(120000, () => {
req.destroy();
file.destroy();
fs.unlink(outputPath, () => {});
reject(new Error("下载超时"));
});
});
if (fs.existsSync(outputPath) && fs.statSync(outputPath).size > 0) {
const sizeMB = (fs.statSync(outputPath).size / (1024 * 1024)).toFixed(2);
info(`下载成功: ${path.basename(outputPath)} (${sizeMB}MB)`);
return true;
} else {
warn("下载文件为空");
}
} catch (err) {
warn(`下载失败: ${err.message}`);
}
if (attempt < maxAttempts) {
const waitTime = attempt * 2;
warn(`等待 ${waitTime}s 后重试...`);
await new Promise((resolve) => setTimeout(resolve, waitTime * 1000));
}
attempt++;
}
error("下载失败,已尝试 ${maxAttempts} 次");
return false;
}
// 获取最新版本
async function getLatestVersion() {
try {
step("检查更新...");
const releasesUrl = `${RELEASE_BASE}/api/${REPO_OWNER}/${REPO_NAME}/releases.json`;
const releases = await fetch(releasesUrl, { json: true, timeout: 30000 });
if (releases && releases.length > 0) {
return releases[0].tag_name;
}
} catch (err) {
debug(`获取版本失败: ${err.message}`);
}
return null;
}
// 检查是否有更新
async function checkForUpdate() {
try {
const latestVersion = await getLatestVersion();
if (!latestVersion) {
info("无法获取最新版本信息");
return;
}
const currentVersion = VERSION.startsWith("v") ? VERSION : `v${VERSION}`;
const latest = latestVersion.startsWith("v") ? latestVersion : `v${latestVersion}`;
info(`当前版本: ${currentVersion}`);
info(`最新版本: ${latest}`);
if (currentVersion === latest) {
info("已经是最新版本");
return;
}
// 简单的版本比较
if (latest > currentVersion) {
warn(`发现新版本: ${latest}`);
warn("运行 'npm update -g @gitlink-ai/cli' 更新");
} else if (latest < currentVersion) {
info("当前版本比最新发布版本更新(开发版本)");
}
} catch (err) {
debug(`检查更新失败: ${err.message}`);
}
}
// 安装二进制
async function installBinary() {
const platform = getPlatformInfo();
info(`平台: ${platform.platform}-${platform.arch}`);
const binaryName = getBinaryName(platform.platform);
const archiveName = getArchiveName(platform.platform, platform.arch);
const npmBinDir = path.dirname(process.execPath);
const installDir = path.join(npmBinDir, "..");
step(`安装二进制到: ${installDir}`);
const version = VERSION.startsWith("v") ? VERSION : `v${VERSION}`;
const binaryUrl = `${RELEASE_BASE}/api/${REPO_OWNER}/${REPO_NAME}/releases/${version}/assets/${archiveName}`;
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "gitlink-cli-"));
const archivePath = path.join(tmpDir, archiveName);
try {
// 下载
const success = await downloadFile(binaryUrl, archivePath);
if (!success) {
throw new Error("下载失败");
}
// 解压
step("解压...");
if (platform.isWindows) {
const AdmZip = require("adm-zip");
const zip = new AdmZip(archivePath);
zip.extractAllTo(installDir, true);
} else {
const tar = require("tar");
await tar.x({
file: archivePath,
cwd: installDir,
strip: 1,
});
}
// 设置执行权限Unix
if (!platform.isWindows) {
const binaryPath = path.join(installDir, binaryName);
if (fs.existsSync(binaryPath)) {
fs.chmodSync(binaryPath, "755");
}
}
info(`二进制安装成功: ${binaryName}`);
} finally {
// 清理临时文件
fs.rmSync(tmpDir, { recursive: true, force: true });
}
}
// 安装skills
async function installSkills() {
const version = VERSION.startsWith("v") ? VERSION : `v${VERSION}`;
const skillsArchive = `${BINARY_NAME}_${version}_skills.zip`;
const skillsUrl = `${RELEASE_BASE}/api/${REPO_OWNER}/${REPO_NAME}/releases/${version}/assets/${skillsArchive}`;
const skillsDir = path.join(os.homedir(), ".gitlink", "skills");
if (!fs.existsSync(skillsDir)) {
fs.mkdirSync(skillsDir, { recursive: true });
}
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "gitlink-skills-"));
const archivePath = path.join(tmpDir, skillsArchive);
try {
step("安装 skills...");
const success = await downloadFile(skillsUrl, archivePath);
if (!success) {
warn("Skills 包下载失败(可稍后手动安装)");
return;
}
// 解压
const AdmZip = require("adm-zip");
const zip = new AdmZip(archivePath);
zip.extractAllTo(skillsDir, true);
info("Skills 安装成功");
} catch (err) {
warn(`Skills 安装失败: ${err.message}`);
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
}
// 验证安装
function verifyInstallation() {
step("验证安装...";
try {
const result = execSync("gitlink-cli version", { encoding: "utf8" });
info(`安装成功! ${result.trim()}`);
return true;
} catch (err) {
warn("验证命令失败(可能需要重启终端)");
return false;
}
}
// 主函数
async function main() {
console.log("");
console.log(" ╔══════════════════════════════════════╗");
console.log(" ║ GitLink CLI npm 安装脚本 ║");
console.log(" ╚══════════════════════════════════════╝");
console.log("");
try {
await installBinary();
await installSkills();
verifyInstallation();
console.log("");
info("快速开始:");
info(" gitlink-cli auth login # 登录账号");
info(" gitlink-cli --help # 查看所有命令");
info(" gitlink-cli version # 查看版本信息");
console.log("");
} catch (err) {
error(`安装失败: ${err.message}`);
process.exit(1);
}
}
// 如果直接运行此脚本
if (require.main === module || process.argv[1].endsWith("update.js")) {
// 检查更新
if (process.argv.includes("--check")) {
(async () => {
await checkForUpdate();
})();
} else {
// 安装
(async () => {
await main();
})();
}
}
module.exports = {
main,
checkForUpdate,
installBinary,
installSkills,
};