- kisync update: show available releases from Gitea - kisync update --apply: download and replace exe in-place - Background update check after every command (non-blocking) - Fix: compute hash from disk-read object (not server object) to prevent false "modified" status after pull due to line-ending differences Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
219 lines
7.2 KiB
TypeScript
219 lines
7.2 KiB
TypeScript
import * as fs from 'fs';
|
||
import * as path from 'path';
|
||
import * as os from 'os';
|
||
import chalk from 'chalk';
|
||
import fetch from 'node-fetch';
|
||
|
||
const GITEA_API = 'https://gitea.esh-service.ru/api/v1';
|
||
const REPO_OWNER = 'public';
|
||
const REPO_NAME = 'api_builder_cli_client';
|
||
const EXE_NAME = 'kisync.exe';
|
||
|
||
interface Release {
|
||
id: number;
|
||
tag_name: string;
|
||
name: string;
|
||
body: string;
|
||
published_at: string;
|
||
assets: {
|
||
id: number;
|
||
name: string;
|
||
size: number;
|
||
browser_download_url: string;
|
||
}[];
|
||
}
|
||
|
||
function parseVersion(tag: string): number[] {
|
||
return tag.replace(/^v/, '').split('.').map(Number);
|
||
}
|
||
|
||
function isNewer(remote: string, local: string): boolean {
|
||
const r = parseVersion(remote);
|
||
const l = parseVersion(local);
|
||
for (let i = 0; i < Math.max(r.length, l.length); i++) {
|
||
const rv = r[i] || 0;
|
||
const lv = l[i] || 0;
|
||
if (rv > lv) return true;
|
||
if (rv < lv) return false;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
async function fetchReleases(): Promise<Release[]> {
|
||
const url = `${GITEA_API}/repos/${REPO_OWNER}/${REPO_NAME}/releases`;
|
||
const res = await fetch(url, { timeout: 5000 });
|
||
if (!res.ok) {
|
||
throw new Error(`HTTP ${res.status}`);
|
||
}
|
||
return res.json() as Promise<Release[]>;
|
||
}
|
||
|
||
/**
|
||
* Фоновая проверка обновлений — вызывается при каждой команде.
|
||
* Не блокирует работу, просто выводит подсказку в конце.
|
||
*/
|
||
export async function checkForUpdateBackground(currentVersion: string): Promise<void> {
|
||
try {
|
||
const releases = await fetchReleases();
|
||
const latest = releases.find(r =>
|
||
r.assets.some(a => a.name.toLowerCase() === EXE_NAME.toLowerCase())
|
||
);
|
||
if (latest && isNewer(latest.tag_name, currentVersion)) {
|
||
console.log('');
|
||
console.log(
|
||
chalk.yellow(` Доступна новая версия: ${latest.tag_name} (текущая: v${currentVersion})`)
|
||
);
|
||
console.log(
|
||
chalk.yellow(` Обновить: kisync update --apply`)
|
||
);
|
||
}
|
||
} catch {
|
||
// Тихо игнорируем — фоновая проверка не должна мешать работе
|
||
}
|
||
}
|
||
|
||
/**
|
||
* kisync update — показать список релизов
|
||
*/
|
||
export async function updateCommand(currentVersion: string): Promise<void> {
|
||
console.log(chalk.gray('Проверка обновлений...'));
|
||
console.log(chalk.gray(`Текущая версия: v${currentVersion}\n`));
|
||
|
||
let releases: Release[];
|
||
try {
|
||
releases = await fetchReleases();
|
||
} catch (err: any) {
|
||
throw new Error(`Не удалось подключиться к серверу обновлений: ${err.message}`);
|
||
}
|
||
|
||
if (releases.length === 0) {
|
||
console.log(chalk.green('Релизы не найдены.'));
|
||
return;
|
||
}
|
||
|
||
console.log(chalk.bold('Доступные релизы:\n'));
|
||
for (const rel of releases) {
|
||
const tag = rel.tag_name;
|
||
const date = new Date(rel.published_at).toLocaleDateString('ru-RU');
|
||
const isCurrent = tag === `v${currentVersion}` || tag === currentVersion;
|
||
const marker = isCurrent ? chalk.green(' ← текущая') : '';
|
||
const newer = isNewer(tag, currentVersion);
|
||
|
||
const prefix = newer ? chalk.cyan(' ↑') : chalk.gray(' ');
|
||
console.log(`${prefix} ${chalk.bold(tag)} ${chalk.gray(date)}${marker}`);
|
||
|
||
if (rel.body) {
|
||
const lines = rel.body.trim().split('\n').slice(0, 3);
|
||
for (const line of lines) {
|
||
console.log(chalk.gray(` ${line}`));
|
||
}
|
||
}
|
||
|
||
if (rel.assets.length > 0) {
|
||
for (const asset of rel.assets) {
|
||
const sizeMb = (asset.size / (1024 * 1024)).toFixed(1);
|
||
console.log(chalk.gray(` ${asset.name} (${sizeMb} МБ)`));
|
||
}
|
||
}
|
||
console.log('');
|
||
}
|
||
|
||
// Найти последний релиз с exe
|
||
const latest = releases.find(r =>
|
||
r.assets.some(a => a.name.toLowerCase() === EXE_NAME.toLowerCase())
|
||
);
|
||
|
||
if (!latest) {
|
||
console.log(chalk.yellow('Ни один релиз не содержит kisync.exe.'));
|
||
return;
|
||
}
|
||
|
||
if (!isNewer(latest.tag_name, currentVersion)) {
|
||
console.log(chalk.green('У вас установлена последняя версия.'));
|
||
return;
|
||
}
|
||
|
||
console.log(
|
||
chalk.cyan(`Новая версия: ${latest.tag_name} (текущая: v${currentVersion})`)
|
||
);
|
||
console.log(
|
||
chalk.gray(`Для обновления выполните: kisync update --apply\n`)
|
||
);
|
||
}
|
||
|
||
/**
|
||
* kisync update --apply — скачать и заменить exe
|
||
*/
|
||
export async function updateApplyCommand(currentVersion: string): Promise<void> {
|
||
console.log(chalk.gray('Проверка обновлений...'));
|
||
|
||
let releases: Release[];
|
||
try {
|
||
releases = await fetchReleases();
|
||
} catch (err: any) {
|
||
throw new Error(`Не удалось подключиться к серверу обновлений: ${err.message}`);
|
||
}
|
||
|
||
const latest = releases.find(r =>
|
||
r.assets.some(a => a.name.toLowerCase() === EXE_NAME.toLowerCase())
|
||
);
|
||
|
||
if (!latest) {
|
||
throw new Error('Ни один релиз не содержит kisync.exe.');
|
||
}
|
||
|
||
if (!isNewer(latest.tag_name, currentVersion)) {
|
||
console.log(chalk.green(`Уже установлена последняя версия (v${currentVersion}).`));
|
||
return;
|
||
}
|
||
|
||
const asset = latest.assets.find(
|
||
a => a.name.toLowerCase() === EXE_NAME.toLowerCase()
|
||
)!;
|
||
|
||
console.log(chalk.cyan(`Скачивание ${latest.tag_name}...`));
|
||
console.log(chalk.gray(` ${asset.browser_download_url}`));
|
||
|
||
const res = await fetch(asset.browser_download_url);
|
||
if (!res.ok) {
|
||
throw new Error(`Ошибка загрузки: HTTP ${res.status}`);
|
||
}
|
||
|
||
const buffer = await res.buffer();
|
||
console.log(chalk.gray(` Загружено ${(buffer.length / (1024 * 1024)).toFixed(1)} МБ`));
|
||
|
||
// Путь к текущему exe
|
||
const currentExe = process.execPath;
|
||
const exeDir = path.dirname(currentExe);
|
||
const exeName = path.basename(currentExe);
|
||
|
||
// На Windows нельзя перезаписать работающий exe — переименуем в .old
|
||
const oldExe = path.join(exeDir, `${exeName}.old`);
|
||
const newExe = path.join(exeDir, exeName);
|
||
|
||
try {
|
||
if (fs.existsSync(oldExe)) {
|
||
fs.unlinkSync(oldExe);
|
||
}
|
||
|
||
fs.renameSync(currentExe, oldExe);
|
||
fs.writeFileSync(newExe, buffer);
|
||
|
||
console.log(chalk.green(`\nОбновлено до ${latest.tag_name}!`));
|
||
console.log(chalk.gray(` Резервная копия: ${oldExe}`));
|
||
console.log(chalk.gray(` Перезапустите kisync для использования новой версии.`));
|
||
} catch (err: any) {
|
||
// Попытка восстановить при ошибке
|
||
if (fs.existsSync(oldExe) && !fs.existsSync(newExe)) {
|
||
try { fs.renameSync(oldExe, newExe); } catch {}
|
||
}
|
||
|
||
// Запасной вариант — сохранить во временную папку
|
||
const tmpPath = path.join(os.tmpdir(), EXE_NAME);
|
||
fs.writeFileSync(tmpPath, buffer);
|
||
console.log(chalk.yellow(`\nНе удалось заменить exe: ${err.message}`));
|
||
console.log(chalk.yellow(`Новая версия сохранена: ${tmpPath}`));
|
||
console.log(chalk.yellow(`Скопируйте вручную в: ${exeDir}\\`));
|
||
}
|
||
}
|