Files
api_builder_cli_client/src/commands/update.ts
eshmeshek 0ad43a6981 Add self-update command and fix hash mismatch after pull
- 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>
2026-03-14 17:41:35 +03:00

219 lines
7.2 KiB
TypeScript
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.

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}\\`));
}
}