forked from Gitlink/gitlink-cli
430 lines
11 KiB
JavaScript
430 lines
11 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 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,
|
||
};
|